Upgrading the RS Models Loader node
This commit is contained in:
+69
-93
@@ -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))
|
||||
+1
-1
@@ -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__}")
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
|
||||
+153
-146
@@ -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 = "<div style='padding:8px;color:#999;text-align:center;'>Loading...</div>";
|
||||
presetListOverlay.style.display = "flex";
|
||||
try {
|
||||
const res = await fetch("/rayko_models/list_presets", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
const endpoint = pendingSelectType === "model" ? "/rayko_models/list_presets" : "/rayko_loras/list_presets";
|
||||
const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" } });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const list = await res.json();
|
||||
presetListOverlay.innerHTML = "";
|
||||
if (!list.length) {
|
||||
@@ -241,91 +264,92 @@ app.registerExtension({
|
||||
nameSpan.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
presetListOverlay.style.display = "none";
|
||||
const res2 = await fetch("/rayko_models/load_preset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
if (res2.ok) {
|
||||
const loadEndpoint = pendingSelectType === "model" ? "/rayko_models/load_preset" : "/rayko_loras/load_preset";
|
||||
try {
|
||||
const res2 = await fetch(loadEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }) });
|
||||
if (!res2.ok) throw new Error(`HTTP ${res2.status}`);
|
||||
const data = await res2.json();
|
||||
applyPresetData(data);
|
||||
if (pendingSelectType === "model") applyPresetData(data);
|
||||
else applyLoraPresetData(data);
|
||||
} catch (err) {
|
||||
console.error(`[Rayko] Failed to load preset:`, err);
|
||||
alert(`Failed to load preset: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBtn = document.createElement("span");
|
||||
deleteBtn.textContent = "❌";
|
||||
deleteBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;";
|
||||
deleteBtn.onmouseenter = () => {
|
||||
deleteBtn.style.opacity = "1";
|
||||
deleteBtn.style.transform = "scale(1.2)";
|
||||
};
|
||||
deleteBtn.onmouseleave = () => {
|
||||
deleteBtn.style.opacity = "0.7";
|
||||
deleteBtn.style.transform = "scale(1)";
|
||||
};
|
||||
deleteBtn.onclick = (e) => {
|
||||
const delBtn = document.createElement("span");
|
||||
delBtn.textContent = "❌";
|
||||
delBtn.style.cssText = "cursor:pointer;margin-left:8px;font-size:14px;opacity:0.7;";
|
||||
delBtn.onmouseenter = () => { delBtn.style.opacity = "1"; delBtn.style.transform = "scale(1.2)"; };
|
||||
delBtn.onmouseleave = () => { delBtn.style.opacity = "0.7"; delBtn.style.transform = "scale(1)"; };
|
||||
delBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
pendingDeleteName = name;
|
||||
pendingDeleteType = pendingSelectType;
|
||||
deleteText.textContent = `Delete "${name}"?`;
|
||||
|
||||
const selectBtnRect = selectPresetBtn.getBoundingClientRect();
|
||||
deleteConfirmOverlay.style.left = selectBtnRect.left + "px";
|
||||
deleteConfirmOverlay.style.top = (selectBtnRect.bottom + 5) + "px";
|
||||
const r = btnElement.getBoundingClientRect();
|
||||
deleteConfirmOverlay.style.left = r.left + "px";
|
||||
deleteConfirmOverlay.style.top = (r.bottom + 5) + "px";
|
||||
deleteConfirmOverlay.style.display = "block";
|
||||
};
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(deleteBtn);
|
||||
row.appendChild(delBtn);
|
||||
presetListOverlay.appendChild(row);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`[Rayko] Failed to list presets:`, e);
|
||||
presetListOverlay.textContent = "Error loading";
|
||||
}
|
||||
};
|
||||
|
||||
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; }
|
||||
pendingSelectType = "model";
|
||||
showPresetList(selectPresetBtn);
|
||||
});
|
||||
|
||||
selectLoraPresetBtn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
presetNameInput.style.display = "none";
|
||||
deleteConfirmOverlay.style.display = "none";
|
||||
if (presetListOverlay.style.display === "flex") { presetListOverlay.style.display = "none"; return; }
|
||||
pendingSelectType = "lora";
|
||||
showPresetList(selectLoraPresetBtn);
|
||||
});
|
||||
|
||||
deleteOk.addEventListener("click", async () => {
|
||||
if (pendingDeleteName) {
|
||||
await fetch("/rayko_models/delete_preset", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: pendingDeleteName })
|
||||
});
|
||||
deleteConfirmOverlay.style.display = "none";
|
||||
presetListOverlay.style.display = "none";
|
||||
pendingDeleteName = null;
|
||||
const endpoint = pendingDeleteType === "model" ? "/rayko_models/delete_preset" : "/rayko_loras/delete_preset";
|
||||
try {
|
||||
const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: pendingDeleteName }) });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
deleteConfirmOverlay.style.display = "none";
|
||||
presetListOverlay.style.display = "none";
|
||||
pendingDeleteName = null;
|
||||
} catch (err) {
|
||||
console.error(`[Rayko] Failed to delete preset:`, err);
|
||||
alert(`Failed to delete preset: ${err.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
deleteCancel.addEventListener("click", () => {
|
||||
deleteConfirmOverlay.style.display = "none";
|
||||
pendingDeleteName = null;
|
||||
});
|
||||
deleteCancel.addEventListener("click", () => { deleteConfirmOverlay.style.display = "none"; pendingDeleteName = null; });
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!presetListOverlay?.contains(e.target)) {
|
||||
presetListOverlay.style.display = "none";
|
||||
}
|
||||
if (!presetNameInput?.contains(e.target)) {
|
||||
presetNameInput.style.display = "none";
|
||||
}
|
||||
if (!deleteConfirmOverlay?.contains(e.target)) {
|
||||
deleteConfirmOverlay.style.display = "none";
|
||||
}
|
||||
if (!presetListOverlay?.contains(e.target)) presetListOverlay.style.display = "none";
|
||||
if (!presetNameInput?.contains(e.target)) presetNameInput.style.display = "none";
|
||||
if (!deleteConfirmOverlay?.contains(e.target)) deleteConfirmOverlay.style.display = "none";
|
||||
});
|
||||
|
||||
const presetsWidget = this.addDOMWidget("presets_ui", "custom", presetsRoot);
|
||||
presetsWidget.computeSize = function() {
|
||||
return [this.width || 130, 50];
|
||||
};
|
||||
|
||||
this.addWidget("button", "✔️ Update LoRA list", "", async () => {
|
||||
await this.loadLoraList();
|
||||
if (this.graph) this.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
|
||||
this.addWidget("button", "➕ Add LoRA", "", () => {
|
||||
this.showLoraTreeSelector();
|
||||
});
|
||||
this.addWidget("button", "➕ Add LoRA", "", () => { this.showLoraTreeSelector(); });
|
||||
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
@@ -373,24 +397,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
return originalOnConfigure ? originalOnConfigure.apply(this, arguments) : undefined;
|
||||
};
|
||||
|
||||
nodeType.prototype.saveToStorage = function() {
|
||||
if (!this.storageKey) return;
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify({
|
||||
loraRows: this.loraRows,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
localStorage.setItem(this.storageKey, JSON.stringify({ loraRows: this.loraRows, timestamp: Date.now() }));
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
@@ -400,9 +416,7 @@ app.registerExtension({
|
||||
const stored = localStorage.getItem(this.storageKey);
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
if (Date.now() - data.timestamp < 86400000 && Array.isArray(data.loraRows)) {
|
||||
return data.loraRows;
|
||||
}
|
||||
if (Date.now() - data.timestamp < 86400000 && Array.isArray(data.loraRows)) return data.loraRows;
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
@@ -414,19 +428,13 @@ app.registerExtension({
|
||||
if (this.properties && this.properties["lora_rows"]) {
|
||||
try {
|
||||
savedData = JSON.parse(this.properties["lora_rows"]);
|
||||
if (Array.isArray(savedData) && savedData.length > 0) {
|
||||
this.loraRows = savedData;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(savedData) && savedData.length > 0) { this.loraRows = savedData; return; }
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!savedData && this.hiddenWidget && this.hiddenWidget.value) {
|
||||
try {
|
||||
const widgetVal = JSON.parse(this.hiddenWidget.value);
|
||||
if (Array.isArray(widgetVal) && widgetVal.length > 0) {
|
||||
this.loraRows = widgetVal;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(widgetVal) && widgetVal.length > 0) { this.loraRows = widgetVal; return; }
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!savedData) {
|
||||
@@ -467,7 +475,6 @@ app.registerExtension({
|
||||
nodeType.prototype.onDrawForeground = function(ctx, visibleRect) {
|
||||
if (this.loraRows.length === 0) return;
|
||||
this.clickZones = [];
|
||||
|
||||
const addButton = this.widgets.find(w => w.name === "➕ Add LoRA");
|
||||
const startY = addButton ? (addButton.y + addButton.height + 15) : 40;
|
||||
const padding = 10;
|
||||
|
||||
Reference in New Issue
Block a user