From 3506f9bf975e7ccac602c98466c1092d6d78dc1a Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:10:18 +0300 Subject: [PATCH] fixed the issue of LoRA data loss in the RS Models Loader node --- Rayko_Models_Loader.py | 10 ++ __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rayko_lora_widget.js | 294 ++++++++++++++++++++++++++++++++------- 5 files changed, 255 insertions(+), 55 deletions(-) diff --git a/Rayko_Models_Loader.py b/Rayko_Models_Loader.py index cd681c6..1ed795e 100644 --- a/Rayko_Models_Loader.py +++ b/Rayko_Models_Loader.py @@ -41,12 +41,17 @@ class RaykoModelsLoader: @classmethod def IS_CHANGED(cls, lora_data="[]", **kwargs): import hashlib + if lora_data is None: + lora_data = "[]" return hashlib.md5(lora_data.encode()).hexdigest() def load_models(self, unet_name, weight_dtype, clip_name, clip_type, clip_device, vae_name, lora_data="[]"): print(f"\n[Rayko] === Start load_models ===") print(f"[Rayko] lora_data RAW: {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}") @@ -60,6 +65,8 @@ class RaykoModelsLoader: 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: loras = [] @@ -68,6 +75,9 @@ class RaykoModelsLoader: lora_loader = LoraLoader() for i, lora in enumerate(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)) diff --git a/__init__.py b/__init__.py index 8fa37bb..d93ae83 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.15.8" +__version__ = "0.15.9" import importlib import logging from pathlib import Path diff --git a/package.json b/package.json index 4379685..50a85db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.15.8", + "version": "0.15.9", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "MIT", diff --git a/pyproject.toml b/pyproject.toml index d4954d3..02d2a05 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.15.8" +version = "0.15.9" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/rayko_lora_widget.js b/web/rayko_lora_widget.js index 5ceb3c4..0c3c758 100644 --- a/web/rayko_lora_widget.js +++ b/web/rayko_lora_widget.js @@ -5,12 +5,16 @@ app.registerExtension({ name: "RaykoLoraWidget", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "RaykoModelsLoader") { - console.log("[Rayko] JS loaded"); + console.log("[Rayko] JS Extension Loaded"); - const onNodeCreated = nodeType.prototype.onNodeCreated; + // Сохраняем оригинальные методы ПЕРЕД переопределением + const originalOnNodeCreated = nodeType.prototype.onNodeCreated; + const originalOnConfigure = nodeType.prototype.onConfigure; + const originalOnSerialize = nodeType.prototype.onSerialize; + const originalOnRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onNodeCreated = function() { - const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + const result = originalOnNodeCreated ? originalOnNodeCreated.apply(this, arguments) : undefined; this.loraRows = []; this.loraOptions = []; @@ -18,13 +22,28 @@ app.registerExtension({ this.targetWidth = 450; this.rowHeight = 30; this.clickZones = []; + this.storageKey = null; + this.isRestoring = false; + this.isInitialized = false; + // Находим виджет lora_data this.hiddenWidget = this.widgets.find(w => w.name === "lora_data"); + + // ВАЖНО: Сохраняем ссылку на ноду для использования в замыкании + const nodeRef = this; + if (this.hiddenWidget) { this.hiddenWidget.hidden = true; - this.hiddenWidget.serializeValue = () => { - this.syncData(); - return this.hiddenWidget.value; + + // ИСПРАВЛЕНИЕ: Используем nodeRef вместо this внутри замыкания + const originalSerializeValue = this.hiddenWidget.serializeValue; + this.hiddenWidget.serializeValue = function() { + // Синхронизируем данные перед сериализацией + nodeRef.syncData(); + + // Возвращаем значение виджета, гарантируем что оно не undefined + const val = nodeRef.hiddenWidget ? nodeRef.hiddenWidget.value : "[]"; + return (val !== undefined && val !== null) ? val : "[]"; }; } @@ -32,33 +51,157 @@ app.registerExtension({ this.addWidget("button", "✔️ Update LoRA list", "", async () => { await this.loadLoraList(); - - this.graph.setDirtyCanvas(true, true); - console.log("[Rayko] LoRA list updated (rows saved)"); + if (this.graph) this.graph.setDirtyCanvas(true, true); + console.log("[Rayko] LoRA list updated"); }); this.addWidget("button", "➕ Add LoRA", "", () => { this.showLoraTreeSelector(); }); - this.loadLoraList().then(() => { - if (this.hiddenWidget && this.hiddenWidget.value) { - try { - const saved = JSON.parse(this.hiddenWidget.value); - if (Array.isArray(saved) && saved.length > 0) { - this.loraRows = saved; - console.log("[Rayko] LoRA restored:", this.loraRows.length); - } - } catch (e) { - console.error("[Rayko] Recovery error:", e); - } + // Инициализация после получения ID + const self = this; + setTimeout(() => { + if (this.id) { + this.storageKey = `rayko_lora_${this.id}`; + console.log("[Rayko] Storage key:", this.storageKey); + + this.loadLoraList().then(() => { + this.restoreData(); + this.isInitialized = true; + self.updateUI(); + }); } - this.updateUI(); - }); + }, 100); return result; }; + // Восстановление данных при загрузке графа + nodeType.prototype.onConfigure = function(info) { + console.log("[Rayko] onConfigure called for node", this.id); + this.isRestoring = true; + + // Восстанавливаем из properties ноды + if (info.properties && info.properties["lora_rows"]) { + try { + const saved = JSON.parse(info.properties["lora_rows"]); + if (Array.isArray(saved)) { + this.loraRows = saved; + console.log("[Rayko] Restored from properties:", this.loraRows.length); + } + } catch (e) { + console.error("[Rayko] Properties restore error:", e); + } + } + + // Проверяем widgets_values + if (info.widgets_values && Array.isArray(info.widgets_values)) { + for (let i = 0; i < info.widgets_values.length; i++) { + const val = info.widgets_values[i]; + if (val && typeof val === "string" && val.startsWith("[")) { + try { + const saved = JSON.parse(val); + if (Array.isArray(saved) && saved.length > 0) { + this.loraRows = saved; + console.log("[Rayko] Restored from widgets_values:", this.loraRows.length); + break; + } + } catch (e) { + // Не ошибка, просто не JSON + } + } + } + } + + this.isRestoring = false; + + // Загружаем список лор и обновляем UI + const self = this; + this.loadLoraList().then(() => { + self.updateUI(); + }); + + return originalOnConfigure ? originalOnConfigure.apply(this, arguments) : undefined; + }; + + // Сохранение в localStorage + nodeType.prototype.saveToStorage = function() { + if (!this.storageKey) return; + try { + const data = { + loraRows: this.loraRows, + timestamp: Date.now() + }; + localStorage.setItem(this.storageKey, JSON.stringify(data)); + } catch (e) { + console.error("[Rayko] Storage save error:", e); + } + }; + + // Восстановление из localStorage + nodeType.prototype.restoreFromStorage = function() { + if (!this.storageKey) return null; + try { + const stored = localStorage.getItem(this.storageKey); + if (stored) { + const data = JSON.parse(stored); + const isFresh = Date.now() - data.timestamp < 86400000; + if (isFresh && Array.isArray(data.loraRows)) { + return data.loraRows; + } + } + } catch (e) { + console.error("[Rayko] Storage restore error:", e); + } + return null; + }; + + // Функция восстановления данных + nodeType.prototype.restoreData = function() { + if (this.isRestoring) return; + + let savedData = null; + + // Приоритет 1: properties ноды + if (this.properties && this.properties["lora_rows"]) { + try { + savedData = JSON.parse(this.properties["lora_rows"]); + if (Array.isArray(savedData) && savedData.length > 0) { + console.log("[Rayko] Restored from properties:", savedData.length); + this.loraRows = savedData; + return; + } + } catch (e) { + console.error("[Rayko] Properties parse error:", e); + } + } + + // Приоритет 2: hiddenWidget + if (!savedData && this.hiddenWidget && this.hiddenWidget.value) { + try { + const widgetVal = JSON.parse(this.hiddenWidget.value); + if (Array.isArray(widgetVal) && widgetVal.length > 0) { + savedData = widgetVal; + console.log("[Rayko] Restored from widget:", savedData.length); + this.loraRows = savedData; + return; + } + } catch (e) { + console.error("[Rayko] Widget parse error:", e); + } + } + + // Приоритет 3: localStorage + if (!savedData) { + savedData = this.restoreFromStorage(); + if (savedData) { + console.log("[Rayko] Restored from localStorage:", savedData.length); + this.loraRows = savedData; + } + } + }; + nodeType.prototype.loadLoraList = async function() { try { const response = await api.fetchApi("/rayko/get_loras"); @@ -67,7 +210,7 @@ app.registerExtension({ this.loraTree = this.buildLoraTree(this.loraOptions); console.log("[Rayko] LoRA found:", this.loraOptions.length); } catch (e) { - console.error("[Rayko] ERROR:", e); + console.error("[Rayko] ERROR loading list:", e); this.loraOptions = []; this.loraTree = {}; } @@ -98,8 +241,10 @@ app.registerExtension({ if (this.loraRows.length === 0) return; this.clickZones = []; - const lastWidget = this.widgets[this.widgets.length - 1]; - const startY = lastWidget.y + lastWidget.height + 15; + const visibleWidgets = this.widgets.filter(w => !w.hidden); + const lastWidget = visibleWidgets.length > 0 ? visibleWidgets[visibleWidgets.length - 1] : null; + + const startY = lastWidget ? (lastWidget.y + lastWidget.height + 15) : 40; const padding = 10; const rightPanelWidth = 180; @@ -180,17 +325,6 @@ app.registerExtension({ if (this.size[1] < totalH) this.setSize([this.targetWidth, totalH]); }; - nodeType.prototype.formatLoraName = function(name) { - if (!name || name === "None") return "None"; - const parts = name.replace(/\\/g, "/").split("/"); - if (parts.length > 1) { - const folder = parts[parts.length - 2]; - const file = parts[parts.length - 1]; - return folder + "/" + file; - } - return name; - }; - nodeType.prototype.onMouseDown = function(e, pos, canvas) { if (!this.clickZones || this.clickZones.length === 0) return false; @@ -202,18 +336,18 @@ app.registerExtension({ if (zone.type === "toggle") { this.loraRows[zone.index].enabled = !this.loraRows[zone.index].enabled; this.syncData(); - this.graph.setDirtyCanvas(true, true); + if (this.graph) this.graph.setDirtyCanvas(true, true); return true; } else if (zone.type === "strength_input") { const currentValue = this.loraRows[zone.index].strength_model; - const newValue = prompt("Enter strenght LoRA:", currentValue.toFixed(2)); + const newValue = prompt("Enter strength LoRA:", currentValue.toFixed(2)); if (newValue !== null) { const parsed = parseFloat(newValue); if (!isNaN(parsed) && parsed >= -10 && parsed <= 10) { this.loraRows[zone.index].strength_model = parsed; this.syncData(); - this.graph.setDirtyCanvas(true, true); + if (this.graph) this.graph.setDirtyCanvas(true, true); } else { alert("Enter a number from -10 to 10"); } @@ -224,18 +358,19 @@ app.registerExtension({ this.loraRows[zone.index].strength_model = Math.max(-10, Math.round((this.loraRows[zone.index].strength_model - 0.05) * 20) / 20); this.syncData(); - this.graph.setDirtyCanvas(true, true); + if (this.graph) this.graph.setDirtyCanvas(true, true); return true; } else if (zone.type === "right") { this.loraRows[zone.index].strength_model = Math.min(10, Math.round((this.loraRows[zone.index].strength_model + 0.05) * 20) / 20); this.syncData(); - this.graph.setDirtyCanvas(true, true); + if (this.graph) this.graph.setDirtyCanvas(true, true); return true; } else if (zone.type === "delete") { this.loraRows.splice(zone.index, 1); + this.syncData(); this.updateUI(); return true; } @@ -265,7 +400,7 @@ app.registerExtension({ if (Object.keys(this.loraTree).length === 0) { const emptyMsg = document.createElement("div"); - emptyMsg.textContent = "⚠️ List is empty"; + emptyMsg.textContent = "⚠️ List is empty (Click Update LoRA list)"; emptyMsg.style.cssText = `padding: 20px; color: #f44336; text-align: center;`; menu.appendChild(emptyMsg); } else { @@ -298,6 +433,7 @@ app.registerExtension({ nodeType.prototype.addLoraRow = function(loraName) { this.loraRows.push({ name: loraName, strength_model: 1.0, strength_clip: 1.0, enabled: true }); + this.syncData(); this.updateUI(); }; @@ -306,28 +442,82 @@ app.registerExtension({ if (this.graph) this.graph.setDirtyCanvas(true, true); }; + // ГЛАВНАЯ ФУНКЦИЯ СОХРАНЕНИЯ nodeType.prototype.syncData = function() { + if (this.isRestoring) return; + + const jsonData = JSON.stringify(this.loraRows); + + // 1. Обновляем скрытый виджет - гарантируем что значение всегда установлено if (this.hiddenWidget) { - this.hiddenWidget.value = JSON.stringify(this.loraRows); - console.log("[Rayko] syncData:", this.hiddenWidget.value); + this.hiddenWidget.value = jsonData; } + + // 2. Обновляем properties ноды + if (!this.properties) this.properties = {}; + this.properties["lora_rows"] = jsonData; + + // 3. Сохраняем в localStorage + this.saveToStorage(); + + if (this.graph) this.graph.setDirtyCanvas(true, false); }; - const onExecute = this.onExecute; - nodeType.prototype.onExecute = function() { - this.syncData(); - return onExecute ? onExecute.apply(this, arguments) : undefined; - }; - - const onSerialize = this.onSerialize; + // Сериализация - данные должны попасть в JSON воркфлоу nodeType.prototype.onSerialize = function(o) { this.syncData(); - return onSerialize ? onSerialize.apply(this, arguments) : undefined; + + // Гарантируем что properties будут сохранены + if (!o.properties) o.properties = {}; + o.properties["lora_rows"] = this.properties["lora_rows"]; + + console.log("[Rayko] onSerialize:", o.properties["lora_rows"]); + + return originalOnSerialize ? originalOnSerialize.apply(this, arguments) : undefined; + }; + + // При удалении ноды очищаем localStorage + nodeType.prototype.onRemoved = function() { + if (this.storageKey) { + localStorage.removeItem(this.storageKey); + console.log("[Rayko] Cleaned localStorage for node:", this.storageKey); + } + return originalOnRemoved ? originalOnRemoved.apply(this, arguments) : undefined; }; } } }); +// Глобальный обработчик visibilitychange +document.addEventListener("visibilitychange", () => { + if (!document.hidden && app && app.graph) { + console.log("[Rayko] Page visible, restoring all nodes"); + setTimeout(() => { + app.graph._nodes.forEach(node => { + if (node.type === "RaykoModelsLoader" && node.restoreData) { + node.restoreData(); + node.updateUI(); + } + }); + }, 200); + } +}); + +// Глобальный обработчик focus +window.addEventListener("focus", () => { + console.log("[Rayko] Window focused"); + if (app && app.graph) { + setTimeout(() => { + app.graph._nodes.forEach(node => { + if (node.type === "RaykoModelsLoader" && node.restoreData) { + node.restoreData(); + node.updateUI(); + } + }); + }, 200); + } +}); + function createTreeItems(path, tree, level, menu, expandedFolders, self, header, noneItem) { const sortedKeys = Object.keys(tree).sort((a, b) => { const aIsFolder = tree[a] !== null;