fixed the issue of LoRA data loss in the RS Models Loader node

This commit is contained in:
Raykosan
2026-04-18 23:10:18 +03:00
parent fc5c0730fd
commit 3506f9bf97
5 changed files with 255 additions and 55 deletions
+10
View File
@@ -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))
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "0.15.8"
__version__ = "0.15.9"
import importlib
import logging
from pathlib import Path
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"]
+242 -52
View File
@@ -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;