From 2ad46f68d0b549f8d631d62249923a0997d27635 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:38:08 +0300 Subject: [PATCH] Upgrading the RS Models Loader node --- Rayko_Models_Loader.py | 162 +++++++++------------ __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rayko_lora_widget.js | 299 ++++++++++++++++++++------------------- 5 files changed, 225 insertions(+), 242 deletions(-) diff --git a/Rayko_Models_Loader.py b/Rayko_Models_Loader.py index 9528ba6..c651017 100644 --- a/Rayko_Models_Loader.py +++ b/Rayko_Models_Loader.py @@ -24,9 +24,10 @@ import aiohttp CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PRESETS_DIR = os.path.join(CURRENT_DIR, "preset_models") +LORA_PRESETS_DIR = os.path.join(PRESETS_DIR, "loras") -if not os.path.exists(PRESETS_DIR): - os.makedirs(PRESETS_DIR) +os.makedirs(PRESETS_DIR, exist_ok=True) +os.makedirs(LORA_PRESETS_DIR, exist_ok=True) class RaykoModelsLoader: @classmethod @@ -36,13 +37,10 @@ class RaykoModelsLoader: vae_files = folder_paths.get_filename_list("vae") vae_files_with_pixel = vae_files + ["pixel_space"] - weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"] - clip_type_opts = [ "stable_diffusion", "stable_cascade", "sd3", "flux", "flux2", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "ltxv", "wan", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit", ] - device_opts = ["default", "cpu", "cuda"] required = { @@ -67,98 +65,51 @@ class RaykoModelsLoader: @classmethod def IS_CHANGED(cls, lora_data="[]", **kwargs): import hashlib - if lora_data is None: - lora_data = "[]" + if lora_data is None: lora_data = "[]" return hashlib.md5(lora_data.encode()).hexdigest() def load_models(self, unet_name, weight_dtype, use_clip2, clip_name, clip_name2, clip_type, clip_device, vae_name, lora_data="[]"): - print(f"\n[Rayko] === Start load_models ===") - print(f"[Rayko] lora_data RAW: {lora_data}") - print(f"[Rayko] use_clip2: {use_clip2}") - - if not lora_data: - lora_data = "[]" + if not lora_data: lora_data = "[]" wd = None if weight_dtype == "default" else weight_dtype model = UNETLoader().load_unet(unet_name=unet_name, weight_dtype=wd)[0] - print(f"[Rayko] UNET: {unet_name}") - dev = None if clip_device == "default" else clip_device if use_clip2 and clip_name2 and clip_name2 != "None": - print(f"[Rayko] Using DualCLIPLoader mode") - print(f"[Rayko] CLIP1: {clip_name}") - print(f"[Rayko] CLIP2: {clip_name2}") - print(f"[Rayko] CLIP type: {clip_type}") - clip = DualCLIPLoader().load_clip( - clip_name1=clip_name, - clip_name2=clip_name2, - type=clip_type, - device=dev - )[0] + clip = DualCLIPLoader().load_clip(clip_name1=clip_name, clip_name2=clip_name2, type=clip_type, device=dev)[0] else: - print(f"[Rayko] Using standard CLIPLoader mode") - print(f"[Rayko] CLIP: {clip_name}") - print(f"[Rayko] CLIP type: {clip_type}") - clip = CLIPLoader().load_clip( - clip_name=clip_name, - type=clip_type, - device=dev - )[0] - print(f"[Rayko] CLIP loaded successfully") + clip = CLIPLoader().load_clip(clip_name=clip_name, type=clip_type, device=dev)[0] if vae_name == "pixel_space": - print(f"[Rayko] Using pixel_space VAE (fake VAE for direct pixel manipulation)") - sd = {} - sd["pixel_space_vae"] = torch.tensor(1.0) + sd = {"pixel_space_vae": torch.tensor(1.0)} vae = comfy.sd.VAE(sd=sd, metadata=None) vae.throw_exception_if_invalid() else: vae = VAELoader().load_vae(vae_name=vae_name)[0] - print(f"[Rayko] VAE: {vae_name}") try: loras = json.loads(lora_data) if lora_data else [] - if not isinstance(loras, list): - loras = [] - print(f"[Rayko] LoRA count: {len(loras)}") - except Exception as e: + if not isinstance(loras, list): loras = [] + except Exception: loras = [] - print(f"[Rayko] Parse error: {e}") lora_loader = LoraLoader() - - for i, lora in enumerate(loras): - if not isinstance(lora, dict): - continue - + for lora in loras: + if not isinstance(lora, dict): continue name = lora.get("name", "") strength_model = float(lora.get("strength_model", 1.0)) strength_clip = float(lora.get("strength_clip", strength_model)) enabled = lora.get("enabled", True) - print(f"[Rayko] LoRA[{i}]: {name} | strength={strength_model} | enabled={enabled}") - if name and name != "None" and enabled: lora_relative_path = name.replace("\\", "/") lora_full_path = folder_paths.get_full_path("loras", lora_relative_path) - if lora_full_path: try: - print(f"[Rayko] Applying LoRA: {lora_relative_path} (strength={strength_model})") - model, clip = lora_loader.load_lora( - model, clip, - lora_relative_path, - strength_model, - strength_clip - ) - print(f"[Rayko] ✓ LoRA applied successfully") + model, clip = lora_loader.load_lora(model, clip, lora_relative_path, strength_model, strength_clip) except Exception as e: - print(f"[Rayko] ✗ Error: {e}") - else: - print(f"[Rayko] ✗ LoRA file not found: {lora_relative_path}") + print(f"[Rayko] ✗ LoRA Error: {e}") - print(f"[Rayko] === load_models completed successfully ===\n") return (model, clip, vae) NODE_CLASS_MAPPINGS = {"RaykoModelsLoader": RaykoModelsLoader} @@ -166,32 +117,18 @@ 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())) + return aiohttp.web.json_response(sorted(folder_paths.get_filename_list("loras"), key=lambda x: x.lower())) +# --- MODEL PRESETS --- @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") + name = "".join(c for c in data.get("name", "").strip() 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) + json.dump({k: data.get(k, "") for k in ["unet_name", "weight_dtype", "use_clip2", "clip_name", "clip_name2", "clip_type", "clip_device", "vae_name"]}, f, indent=2) return aiohttp.web.Response(status=200, text="OK") except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) @@ -199,9 +136,7 @@ async def rayko_models_save_preset(request): @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')] + presets = [f[:-5] for f in os.listdir(PRESETS_DIR) if f.endswith('.json')] if os.path.exists(PRESETS_DIR) else [] 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)) @@ -209,13 +144,11 @@ async def rayko_models_list_presets(request): @PromptServer.instance.routes.post("/rayko_models/load_preset") async def rayko_models_load_preset(request): try: - data = await request.json() - name = data.get("name") + name = (await request.json()).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.json_response(json.load(f)) return aiohttp.web.Response(status=404, text="Preset not found") except Exception as e: return aiohttp.web.Response(status=500, text=str(e)) @@ -223,14 +156,57 @@ async def rayko_models_load_preset(request): @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") + name = (await request.json()).get("name") 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)) + +# --- LORA PRESETS --- +@PromptServer.instance.routes.post("/rayko_loras/save_preset") +async def rayko_loras_save_preset(request): + try: + data = await request.json() + name = "".join(c for c in data.get("name", "").strip() if c.isalnum() or c in " _-").strip() + if not name: return aiohttp.web.Response(status=400, text="Invalid name") + filepath = os.path.join(LORA_PRESETS_DIR, f"{name}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump({"lora_rows": data.get("lora_rows", [])}, 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_loras/list_presets") +async def rayko_loras_list_presets(request): + try: + presets = [f[:-5] for f in os.listdir(LORA_PRESETS_DIR) if f.endswith('.json')] if os.path.exists(LORA_PRESETS_DIR) else [] + 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_loras/load_preset") +async def rayko_loras_load_preset(request): + try: + name = (await request.json()).get("name") + filepath = os.path.join(LORA_PRESETS_DIR, f"{name}.json") + if os.path.exists(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + return aiohttp.web.json_response(json.load(f)) + 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_loras/delete_preset") +async def rayko_loras_delete_preset(request): + try: + name = (await request.json()).get("name") + filepath = os.path.join(LORA_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/__init__.py b/__init__.py index 2cbb5ab..f3bbbed 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.30.0" +__version__ = "0.30.1" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 58e4cc6..a5ad005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.30.0", + "version": "0.30.1", "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 b35816d..38abae6 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.30.0" +version = "0.30.1" 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 62b060d..685cfc4 100644 --- a/web/rayko_lora_widget.js +++ b/web/rayko_lora_widget.js @@ -40,25 +40,15 @@ app.registerExtension({ if (useClip2Widget && clipName2Widget) { clipName2Widget.disabled = !useClip2Widget.value; - if (clipName2Widget.element) { - if (clipName2Widget.disabled) { - clipName2Widget.element.classList.add("comfy-disabled"); - } else { - clipName2Widget.element.classList.remove("comfy-disabled"); - } + clipName2Widget.element.classList.toggle("comfy-disabled", clipName2Widget.disabled); } - const originalCallback = useClip2Widget.callback; useClip2Widget.callback = function(value) { if (originalCallback) originalCallback(value); clipName2Widget.disabled = !value; if (clipName2Widget.element) { - if (clipName2Widget.disabled) { - clipName2Widget.element.classList.add("comfy-disabled"); - } else { - clipName2Widget.element.classList.remove("comfy-disabled"); - } + clipName2Widget.element.classList.toggle("comfy-disabled", clipName2Widget.disabled); } nodeRef.graph?.setDirtyCanvas(true, true); }; @@ -66,18 +56,16 @@ 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 collectPresetData = () => ({ + 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) => { @@ -87,21 +75,30 @@ app.registerExtension({ 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); - + ["unet_name", "weight_dtype", "use_clip2", "clip_name", "clip_name2", "clip_type", "clip_device", "vae_name"].forEach(k => setWidgetValue(k, data[k])); this.loraRows = []; this.syncData(); - this.updateUI(); - if (this.graph) this.graph.setDirtyCanvas(true, true); + }; + + const collectLoraPresetData = () => ({ + lora_rows: this.loraRows.map(row => ({ + name: row.name, + strength_model: row.strength_model, + strength_clip: row.strength_clip, + enabled: row.enabled + })) + }); + + const applyLoraPresetData = (data) => { + this.loraRows = (data.lora_rows || []).map(row => ({ + name: row.name || "", + strength_model: parseFloat(row.strength_model) || 1.0, + strength_clip: parseFloat(row.strength_clip) || 1.0, + enabled: row.enabled !== undefined ? row.enabled : true + })); + this.syncData(); + this.updateUI(); }; const presetListOverlay = document.createElement("div"); @@ -154,75 +151,101 @@ app.registerExtension({ deleteConfirmOverlay.append(deleteText, deleteBtns); document.body.appendChild(deleteConfirmOverlay); + let pendingSaveType = "model"; + let pendingSelectType = "model"; + let pendingDeleteType = "model"; let pendingDeleteName = null; + const presetsWrapper = document.createElement("div"); + presetsWrapper.style.cssText = "display:flex;flex-direction:column;gap:4px;width:100%;margin:0;padding:0;box-sizing:border-box;"; + + // --- MODEL PRESETS ROW --- 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;"; + presetsRoot.style.cssText = "display:flex;gap:4px;width:100%;align-items:center;height:30px;"; const savePresetBtn = document.createElement("button"); - savePresetBtn.textContent = " Save preset"; + savePresetBtn.textContent = " 💾 Save models 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.textContent = " 📂 Select models 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(); + // --- LORA PRESETS ROW (Pale Green) --- + const loraPresetsRoot = document.createElement("div"); + loraPresetsRoot.style.cssText = "display:flex;gap:4px;width:100%;align-items:center;height:30px;"; + + const saveLoraPresetBtn = document.createElement("button"); + saveLoraPresetBtn.textContent = " 💾 Save LoRA preset"; + saveLoraPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #50cc90;border-radius:5px;background:#1a3a2a;color:#aaffcc;cursor:pointer;height:26px;margin:0;"; + + const selectLoraPresetBtn = document.createElement("button"); + selectLoraPresetBtn.textContent = " 📂 Select LoRA preset"; + selectLoraPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #50cc90;border-radius:5px;background:#1a3a2a;color:#aaffcc;cursor:pointer;height:26px;margin:0;"; + loraPresetsRoot.append(saveLoraPresetBtn, selectLoraPresetBtn); + + presetsWrapper.append(presetsRoot, loraPresetsRoot); + + const presetsWidget = this.addDOMWidget("presets_ui", "custom", presetsWrapper); + presetsWidget.computeSize = function() { return [this.width || 130, 70]; }; + + const openSaveDialog = (type, btnElement) => { 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"; + pendingSaveType = type; + const rect = btnElement.getBoundingClientRect(); + presetNameInput.style.left = rect.left + "px"; + presetNameInput.style.top = (rect.bottom + 5) + "px"; presetNameInput.style.display = "block"; inputField.value = ""; setTimeout(() => inputField.focus(), 50); - }); + }; + + savePresetBtn.addEventListener("click", (e) => { e.stopPropagation(); openSaveDialog("model", savePresetBtn); }); + saveLoraPresetBtn.addEventListener("click", (e) => { e.stopPropagation(); openSaveDialog("lora", saveLoraPresetBtn); }); const performSave = () => { const name = inputField.value.trim(); if (!name) return; presetNameInput.style.display = "none"; - const presetData = collectPresetData(); - fetch("/rayko_models/save_preset", { + + const endpoint = pendingSaveType === "model" ? "/rayko_models/save_preset" : "/rayko_loras/save_preset"; + const payload = pendingSaveType === "model" ? { name, ...collectPresetData() } : { name, ...collectLoraPresetData() }; + + fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, ...presetData }) + body: JSON.stringify(payload) + }) + .then(res => { + if (!res.ok) { + return res.text().then(text => { throw new Error(text || res.statusText); }); + } + }) + .catch(err => { + console.error(`[Rayko] Failed to save preset:`, err); + alert(`Failed to save preset:\n${err.message}`); }); }; inputOk.addEventListener("click", performSave); - inputCancel.addEventListener("click", () => { - presetNameInput.style.display = "none"; - }); + 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"; - + const showPresetList = async (btnElement) => { + const rect = btnElement.getBoundingClientRect(); + presetListOverlay.style.left = rect.left + "px"; + presetListOverlay.style.top = (rect.bottom + 5) + "px"; presetListOverlay.innerHTML = "