add basics for image preview
This commit is contained in:
+326
-426
@@ -1,438 +1,338 @@
|
||||
// KVTools: KVGet UI — dropdown + live preview + robust sync + defaults + optional auto-run
|
||||
// Served at /extensions/ComfyUI-KVTools/*
|
||||
# ComfyUI-KVTools: Key/Value Utilities + sicherer Image-Preview-Loader
|
||||
# Robuste Version: gibt NIE None als IMAGE zurück (immer Placeholder).
|
||||
# UI/Dropdown/Defaults/Auto-Run ist in web/extension.js.
|
||||
|
||||
import { app } from "/scripts/app.js";
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
const EXT_NAME = "ComfyUI-KVTools.UI";
|
||||
print("[KVTools] loaded from", __file__)
|
||||
|
||||
// ------------------------------ Registry cache ------------------------------
|
||||
let REGISTRY = null;
|
||||
async function loadRegistry() {
|
||||
const paths = [
|
||||
"/extensions/ComfyUI-KVTools/kv_registry.json",
|
||||
"/extensions/ComfyUI-KVTools/web/kv_registry.json",
|
||||
"/extensions/comfyui-kvtools/kv_registry.json",
|
||||
"/extensions/comfyui-kvtools/web/kv_registry.json",
|
||||
];
|
||||
for (const p of paths) {
|
||||
try {
|
||||
const r = await fetch(p, { cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const j = await r.json();
|
||||
if (j && j.files) { REGISTRY = j; return REGISTRY; }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
REGISTRY = { files: {} };
|
||||
return REGISTRY;
|
||||
}
|
||||
# ---------------------------------------------------------------------
|
||||
# Parsing/Dumping Helpers
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
// ------------------------------ Helpers ------------------------------
|
||||
function W(node, name) { return (node.widgets || []).find(w => w.name === name); }
|
||||
function hideWidget(w) {
|
||||
if (!w) return;
|
||||
w.hidden = true;
|
||||
w.widget_options = { hidden: true };
|
||||
w.draw = () => {};
|
||||
w.computeSize = () => [0, -4];
|
||||
}
|
||||
function moveWidgetToTop(node, w) {
|
||||
if (!node.widgets || !w) return;
|
||||
const i = node.widgets.indexOf(w);
|
||||
if (i > 0) { node.widgets.splice(i, 1); node.widgets.unshift(w); }
|
||||
}
|
||||
function setDropdownValues(combo, values) {
|
||||
combo.options = combo.options || {};
|
||||
combo.options.values = Array.isArray(values) ? values.slice() : [];
|
||||
if (!combo.options.values.includes(combo.value)) {
|
||||
combo.value = combo.options.values[0] ?? "";
|
||||
}
|
||||
}
|
||||
function ensureAsTypeDefault(node) {
|
||||
const w = W(node, "as_type");
|
||||
if (w && !["string", "int", "float", "bool"].includes(String(w.value))) {
|
||||
w.value = "string";
|
||||
const i = node.widgets.indexOf(w);
|
||||
if (i >= 0 && Array.isArray(node.widgets_values)) node.widgets_values[i] = "string";
|
||||
}
|
||||
}
|
||||
function upstreamNode(node) {
|
||||
const inp = (node.inputs || []).find(i => i && i.name === "store");
|
||||
if (!inp || inp.link == null) return null;
|
||||
const link = node.graph.links[inp.link];
|
||||
return link ? node.graph.getNodeById(link.origin_id) : null;
|
||||
}
|
||||
def _parse_data(data: str, fmt: str):
|
||||
data = (data or "").strip()
|
||||
if fmt == "auto":
|
||||
if (data.startswith("{") and data.endswith("}")) or (data.startswith("[") and data.endswith("]")):
|
||||
fmt = "json"
|
||||
else:
|
||||
fmt = "kv"
|
||||
if fmt == "json":
|
||||
try:
|
||||
obj = json.loads(data) if data else {}
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
raise ValueError("JSON is not an object (dict).")
|
||||
except Exception as e:
|
||||
raise ValueError(f"JSON-Error: {e}")
|
||||
elif fmt == "kv":
|
||||
out = {}
|
||||
for line in data.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
m = re.match(r"^\s*([^=:#]+)\s*[:=]\s*(.*)\s*$", line)
|
||||
if not m:
|
||||
raise ValueError(f"not a valid KV-line: {line}")
|
||||
k, v = m.group(1).strip(), m.group(2).strip()
|
||||
out[k] = v
|
||||
return out
|
||||
else:
|
||||
raise ValueError(f"Unknown Format: {fmt}")
|
||||
|
||||
// ---- Inline parse ----
|
||||
function parseStoreObject(text) {
|
||||
const s = String(text ?? "").trim();
|
||||
if (!s) return {};
|
||||
try {
|
||||
const obj = JSON.parse(s);
|
||||
if (obj && typeof obj === "object" && !Array.isArray(obj)) return obj;
|
||||
} catch {}
|
||||
const out = {};
|
||||
for (const raw of s.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const m = line.match(/^\s*([^=:#]+)\s*[:=]\s*(.*)\s*$/);
|
||||
if (m) out[String(m[1]).trim()] = String(m[2]).trim();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function keysFromInline(up) {
|
||||
const dataW = W(up, "data");
|
||||
if (!dataW) return [];
|
||||
const obj = parseStoreObject(dataW.value ?? "");
|
||||
return Object.keys(obj).sort();
|
||||
}
|
||||
def _dump_data(obj: dict, fmt: str, pretty: bool = True) -> str:
|
||||
if fmt == "json":
|
||||
return json.dumps(obj, ensure_ascii=False, indent=(2 if pretty else None))
|
||||
elif fmt == "kv":
|
||||
lines = []
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
v = json.dumps(v, ensure_ascii=False)
|
||||
lines.append(f"{k}={v}")
|
||||
return "\n".join(lines)
|
||||
else:
|
||||
raise ValueError(f"Unknown Format to write: {fmt}")
|
||||
|
||||
// ---- Registry keys ----
|
||||
function upstreamFileName(node) {
|
||||
const up = upstreamNode(node);
|
||||
if (!up) return null;
|
||||
const fnW = W(up, "file_name");
|
||||
if (fnW && fnW.value) return String(fnW.value).trim();
|
||||
const pathW = W(up, "path");
|
||||
if (pathW && pathW.value) {
|
||||
const p = String(pathW.value).trim();
|
||||
const parts = p.split(/[\\/]/);
|
||||
return parts[parts.length - 1] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function keysFromRegistryByFile(fileName) {
|
||||
if (!fileName) return [];
|
||||
const entry = REGISTRY?.files?.[fileName];
|
||||
return Array.isArray(entry?.keys) ? entry.keys.slice() : [];
|
||||
}
|
||||
def _cast(value, as_type: str):
|
||||
if as_type == "string":
|
||||
return "" if value is None else str(value)
|
||||
if as_type == "int":
|
||||
return int(value)
|
||||
if as_type == "float":
|
||||
return float(value)
|
||||
if as_type == "bool":
|
||||
s = str(value).strip().lower()
|
||||
return s in ("1", "true", "yes", "y", "on")
|
||||
return value
|
||||
|
||||
// ---- Unified keys collector ----
|
||||
function collectKeys(node) {
|
||||
const up = upstreamNode(node);
|
||||
if (!up) return [];
|
||||
if (up.comfyClass === "KVLoadInline") return keysFromInline(up);
|
||||
if (up.comfyClass === "KVLoadFromRegistry") {
|
||||
const fname = upstreamFileName(node);
|
||||
return keysFromRegistryByFile(fname);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
# ---------------------------------------------------------------------
|
||||
# Nodes: KV
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
// ---- Default logic aware picker ----
|
||||
function pickNewKey(node, list) {
|
||||
if (!Array.isArray(list) || !list.length) return "";
|
||||
const dkW = W(node, "default_key");
|
||||
const dk = String(dkW?.value || "").trim();
|
||||
if (dk && list.includes(dk)) return dk;
|
||||
const ks = W(node, "key_select");
|
||||
if (ks && list.includes(String(ks.value))) return String(ks.value);
|
||||
return list[0];
|
||||
}
|
||||
|
||||
// ------------------------------ Preview ------------------------------
|
||||
function ensurePreviewWidget(node) {
|
||||
let w = W(node, "_kvtools_preview_value");
|
||||
if (!w) {
|
||||
w = node.addWidget("text", "_kvtools_preview_value", "", null);
|
||||
w.serialize = false;
|
||||
}
|
||||
try { if (w.inputEl) { w.inputEl.readOnly = true; w.inputEl.style.opacity = 0.9; } } catch {}
|
||||
return w;
|
||||
}
|
||||
|
||||
// Optional server routes — if present, we will use them.
|
||||
async function serverRefreshRegistry() { try { await fetch("/kvtools/refresh_registry", { method: "POST" }); } catch {} }
|
||||
async function serverPeek(fileName, key) {
|
||||
if (!fileName || !key) return "";
|
||||
try {
|
||||
const r = await fetch("/kvtools/peek", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ file_name: fileName, key })
|
||||
});
|
||||
if (!r.ok) return "";
|
||||
const j = await r.json();
|
||||
return j && j.ok ? String(j.value ?? "") : "";
|
||||
} catch { return ""; }
|
||||
}
|
||||
|
||||
async function updatePreview(node) {
|
||||
const pv = ensurePreviewWidget(node);
|
||||
const keyW = W(node, "key");
|
||||
const key = String(keyW?.value || "").trim();
|
||||
if (!key) { pv.value = ""; node.setDirtyCanvas(true, true); return; }
|
||||
|
||||
const up = upstreamNode(node);
|
||||
if (!up) { pv.value = ""; node.setDirtyCanvas(true, true); return; }
|
||||
|
||||
// Inline: parse directly
|
||||
if (up.comfyClass === "KVLoadInline") {
|
||||
const dataW = W(up, "data");
|
||||
const obj = parseStoreObject(dataW?.value ?? "");
|
||||
let v = obj[key];
|
||||
if (v && typeof v === "object") { try { v = JSON.stringify(v); } catch {} }
|
||||
pv.value = String(v ?? "");
|
||||
node.setDirtyCanvas(true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Registry: try server peek (if endpoints exist)
|
||||
if (up.comfyClass === "KVLoadFromRegistry") {
|
||||
await serverRefreshRegistry().catch(() => {});
|
||||
await loadRegistry().catch(() => {});
|
||||
const fileName = upstreamFileName(node);
|
||||
const v = await serverPeek(fileName, key).catch(() => "");
|
||||
pv.value = String(v || "");
|
||||
node.setDirtyCanvas(true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
pv.value = "";
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// ------------------------------ Auto-run (to update outputs) ------------------------------
|
||||
function ensureAutoRunToggle(node) {
|
||||
let w = W(node, "_kvtools_autorun");
|
||||
if (!w) {
|
||||
w = node.addWidget("checkbox", "_kvtools_autorun", false, null, { label: "Auto run on change" });
|
||||
w.serialize = true;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
async function tryQueueRun() {
|
||||
if (app?.queuePrompt) {
|
||||
try { await app.queuePrompt(); return; } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------ Defaults (storage + buttons) ------------------------------
|
||||
function ensureDefaultKeyStorage(node) {
|
||||
let w = W(node, "default_key");
|
||||
if (!w) {
|
||||
w = node.addWidget("text", "default_key", "", null);
|
||||
w.serialize = true; // persist in workflow
|
||||
}
|
||||
hideWidget(w);
|
||||
return w;
|
||||
}
|
||||
function ensureDefaultButtons(node) {
|
||||
if (!node.__kvtools_setdef_btn) {
|
||||
const b = node.addWidget("button", "Set default (current key)", null, () => {
|
||||
const ks = W(node, "key_select");
|
||||
const dk = ensureDefaultKeyStorage(node);
|
||||
if (ks && dk) {
|
||||
dk.value = String(ks.value || "");
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
});
|
||||
b.serialize = false;
|
||||
node.__kvtools_setdef_btn = b;
|
||||
}
|
||||
if (!node.__kvtools_loadd_btn) {
|
||||
const b2 = node.addWidget("button", "Load default", null, async () => {
|
||||
const dk = W(node, "default_key");
|
||||
const ks = W(node, "key_select");
|
||||
const keyW = W(node, "key");
|
||||
if (dk && ks && keyW) {
|
||||
const list = collectKeys(node);
|
||||
const val = String(dk.value || "").trim();
|
||||
if (val && list.includes(val)) {
|
||||
ks.value = val;
|
||||
keyW.value = val;
|
||||
node.setDirtyCanvas(true, true);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
class KVLoadInline:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"data": ("STRING", {"multiline": True, "placeholder":
|
||||
"{\n \"speaker\":\"Tom\",\n \"lang\":\"de\"\n}\n"
|
||||
"# oder:\n# speaker=Tom\n# lang=de"}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
b2.serialize = false;
|
||||
node.__kvtools_loadd_btn = b2;
|
||||
}
|
||||
RETURN_TYPES = ("KV",)
|
||||
RETURN_NAMES = ("store",)
|
||||
FUNCTION = "create"
|
||||
CATEGORY = "Utils/KV"
|
||||
|
||||
def create(self, data):
|
||||
store = _parse_data(data, "auto")
|
||||
return (store,)
|
||||
|
||||
class KVGet:
|
||||
"""Read one value from a key/value store"""
|
||||
CATEGORY = "KVTools"
|
||||
FUNCTION = "kv_get"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"store": ("KV",),
|
||||
"key": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"forceInput": False,
|
||||
}),
|
||||
"default": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
}),
|
||||
"as_type": (["string", "int", "float", "bool"], {
|
||||
"default": "string",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"keys_hint": ("STRING", {
|
||||
"default": "(dropdown enabled)",
|
||||
"multiline": False,
|
||||
"visible": False,
|
||||
}),
|
||||
"default_key": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"visible": False,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("value", "key")
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def __init__(self):
|
||||
self.default = ""
|
||||
|
||||
def kv_get(self, store, key, default, as_type, **kwargs):
|
||||
if default is None:
|
||||
default = ""
|
||||
value = store.get(key, default)
|
||||
|
||||
if as_type == "int":
|
||||
try:
|
||||
value = str(int(value))
|
||||
except:
|
||||
value = default
|
||||
elif as_type == "float":
|
||||
try:
|
||||
value = str(float(value))
|
||||
except:
|
||||
value = default
|
||||
elif as_type == "bool":
|
||||
if str(value).strip().lower() in ("1", "true", "yes"):
|
||||
value = "true"
|
||||
else:
|
||||
value = "false"
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
return (value, key)
|
||||
|
||||
class KVLoadFromRegistry:
|
||||
_BASE = os.path.join(os.getcwd(), "custom_kv_stores")
|
||||
try:
|
||||
os.makedirs(_BASE, exist_ok=True)
|
||||
_FILES = sorted([n for n in os.listdir(_BASE) if n.lower().endswith(".json")]) or ["(none)"]
|
||||
except Exception:
|
||||
_FILES = ["(none)"]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_name": (cls._FILES, {"default": cls._FILES[0]}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("KV", "STRING")
|
||||
RETURN_NAMES = ("store", "path")
|
||||
FUNCTION = "load"
|
||||
CATEGORY = "Utils/KV"
|
||||
|
||||
def load(self, file_name):
|
||||
if not file_name or file_name == "(none)":
|
||||
return ({}, "")
|
||||
path = os.path.join(self._BASE, file_name)
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"KVLoadFromRegistry: File not found: {path}")
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
store = _parse_data(text, "auto")
|
||||
return (store, path)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Sicherer Image-Preview (nur unter custom_kv_stores/images)
|
||||
# Struktur: <ComfyUI>/custom_kv_stores/images/<json-basisname>/<key>.png
|
||||
# Immer gültiges IMAGE zurückgeben (Platzhalter bei Missing/Error).
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _images_root():
|
||||
return os.path.join(os.getcwd(), "custom_kv_stores", "images")
|
||||
|
||||
def _sanitize_name(s: str) -> str:
|
||||
s = os.path.basename(str(s or "").strip())
|
||||
return re.sub(r"[^A-Za-z0-9._ \-]+", "_", s)
|
||||
|
||||
# Platzhalter vorab bauen (1x1 schwarz), damit wir nie None zurückgeben
|
||||
try:
|
||||
import torch as _torch
|
||||
_KVTOOLS_PLACEHOLDER = _torch.zeros((1, 1, 1, 3), dtype=_torch.float32) # [B=1,H=1,W=1,C=3]
|
||||
except Exception:
|
||||
_KVTOOLS_PLACEHOLDER = None # Sollte in ComfyUI nicht passieren; als Fallback unten nochmal abgefangen.
|
||||
|
||||
def _placeholder_image():
|
||||
global _KVTOOLS_PLACEHOLDER
|
||||
if _KVTOOLS_PLACEHOLDER is None:
|
||||
# letztes Sicherheitsnetz, falls torch import fehlschlug
|
||||
try:
|
||||
import torch
|
||||
_KVTOOLS_PLACEHOLDER = torch.zeros((1, 1, 1, 3), dtype=torch.float32)
|
||||
except Exception:
|
||||
# letzte Instanz: gib irgendwas zurück, was kein None ist (riskant, aber verhindert None)
|
||||
_KVTOOLS_PLACEHOLDER = 0
|
||||
return _KVTOOLS_PLACEHOLDER
|
||||
|
||||
try:
|
||||
os.makedirs(_images_root(), exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class KVPreviewImageFromRegistry:
|
||||
"""
|
||||
Sucht Bild unter:
|
||||
<ComfyUI>/custom_kv_stores/images/<json-basisname>/<key>.png
|
||||
Gibt bei fehlender Datei KEINEN Fehler aus, sondern einen 1x1-Platzhalter als IMAGE
|
||||
sowie einen leeren Pfad (""). Bei Erfolg: echtes Bild + voller Pfad.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"key": ("STRING", {"forceInput": True, "multiline": False}),
|
||||
"registry_path": ("STRING", {"forceInput": True, "multiline": False}),
|
||||
"ext": (["png"], {"default": "png"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("image", "path")
|
||||
FUNCTION = "load"
|
||||
CATEGORY = "Utils/KV"
|
||||
|
||||
def load(self, key, registry_path, ext):
|
||||
# Standard-Outputs vorbereiten
|
||||
img_out = _placeholder_image()
|
||||
path_out = ""
|
||||
|
||||
try:
|
||||
key = str(key or "").strip()
|
||||
registry_path = str(registry_path or "").strip()
|
||||
|
||||
if not key or not registry_path:
|
||||
return (img_out, path_out)
|
||||
|
||||
base = _images_root()
|
||||
json_base = os.path.splitext(os.path.basename(registry_path))[0]
|
||||
folder = _sanitize_name(json_base)
|
||||
fname = f"{_sanitize_name(key)}.{ext}"
|
||||
full_path = os.path.join(base, folder, fname)
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
return (img_out, path_out) # Platzhalter + leerer Pfad
|
||||
|
||||
# Bild laden
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import torch
|
||||
img = Image.open(full_path).convert("RGB")
|
||||
arr = (np.asarray(img).astype("float32") / 255.0) # H,W,3
|
||||
t = torch.from_numpy(arr)[None, ...] # 1,H,W,3
|
||||
return (t, full_path)
|
||||
|
||||
except Exception:
|
||||
# Niemals None als IMAGE zurückgeben
|
||||
return (img_out, path_out)
|
||||
|
||||
class KVImagePathFromRegistry:
|
||||
"""
|
||||
Baut nur den Pfad (STRING), ohne zu laden:
|
||||
<ComfyUI>/custom_kv_stores/images/<json-basisname>/<key>.png
|
||||
Falls die Datei fehlt, wird trotzdem ein String-Pfad zurückgegeben.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"key": ("STRING", {"forceInput": True, "multiline": False}),
|
||||
"registry_path": ("STRING", {"forceInput": True, "multiline": False}),
|
||||
"ext": (["png"], {"default": "png"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("path",)
|
||||
FUNCTION = "build"
|
||||
CATEGORY = "Utils/KV"
|
||||
|
||||
def build(self, key, registry_path, ext):
|
||||
key = str(key or "").strip()
|
||||
registry_path = str(registry_path or "").strip()
|
||||
if not key or not registry_path:
|
||||
return ("",)
|
||||
base = _images_root()
|
||||
json_base = os.path.splitext(os.path.basename(registry_path))[0]
|
||||
folder = _sanitize_name(json_base)
|
||||
fname = f"{_sanitize_name(key)}.{ext}"
|
||||
path = os.path.join(base, folder, fname)
|
||||
return (path,)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Node Mappings
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KVLoadInline": KVLoadInline,
|
||||
"KVLoadFromRegistry": KVLoadFromRegistry,
|
||||
"KVGet": KVGet,
|
||||
"KVPreviewImageFromRegistry": KVPreviewImageFromRegistry,
|
||||
"KVImagePathFromRegistry": KVImagePathFromRegistry,
|
||||
}
|
||||
|
||||
// ------------------------------ UI build / sync ------------------------------
|
||||
function buildOrEnsureCombo(node) {
|
||||
if (node.__kvtools_combo && node.widgets.includes(node.__kvtools_combo)) {
|
||||
return node.__kvtools_combo;
|
||||
}
|
||||
const combo = node.addWidget("combo", "key_select", "", null, { values: [] });
|
||||
combo.serialize = false;
|
||||
node.__kvtools_combo = combo;
|
||||
moveWidgetToTop(node, combo);
|
||||
return combo;
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KVLoadInline": "KV Load Inline",
|
||||
"KVLoadFromRegistry": "KV Load from Registry",
|
||||
"KVGet": "KV Get Value",
|
||||
"KVPreviewImageFromRegistry": "KV Preview Image (Registry Key)",
|
||||
"KVImagePathFromRegistry": "KV Image Path (Registry Key)",
|
||||
}
|
||||
|
||||
async function syncKeyList(node) {
|
||||
const ks = buildOrEnsureCombo(node);
|
||||
const keyW = W(node, "key");
|
||||
if (!ks || !keyW) return;
|
||||
|
||||
const list = collectKeys(node);
|
||||
setDropdownValues(ks, list);
|
||||
|
||||
const cur = String(keyW.value || "").trim();
|
||||
if (list.includes(cur)) {
|
||||
ks.value = cur;
|
||||
} else {
|
||||
const pick = pickNewKey(node, list); // prefers default_key if valid
|
||||
ks.value = pick;
|
||||
keyW.value = pick;
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
function hideBuiltIns(node) {
|
||||
hideWidget(W(node, "key"));
|
||||
hideWidget(W(node, "keys_hint"));
|
||||
hideWidget(W(node, "default"));
|
||||
hideWidget(W(node, "default_key"));
|
||||
setTimeout(() => {
|
||||
hideWidget(W(node, "key"));
|
||||
hideWidget(W(node, "keys_hint"));
|
||||
hideWidget(W(node, "default"));
|
||||
hideWidget(W(node, "default_key"));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function ensureRefreshButton(node) {
|
||||
if (node.__kvtools_refresh_btn) return node.__kvtools_refresh_btn;
|
||||
const btn = node.addWidget("button", "KVTools: refresh keys", null, async () => {
|
||||
await loadRegistry();
|
||||
await syncKeyList(node);
|
||||
ensureAsTypeDefault(node);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
});
|
||||
btn.serialize = false;
|
||||
node.__kvtools_refresh_btn = btn;
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Upstream changes listeners
|
||||
function hookUpstream(node) {
|
||||
const up = upstreamNode(node);
|
||||
if (!up) return;
|
||||
|
||||
// Inline: react on text changes
|
||||
if (up.comfyClass === "KVLoadInline") {
|
||||
const dataW = W(up, "data");
|
||||
if (dataW && !dataW._kvtoolsHooked) {
|
||||
dataW._kvtoolsHooked = true;
|
||||
const orig = dataW.callback;
|
||||
dataW.callback = async () => {
|
||||
if (orig) orig();
|
||||
await syncKeyList(node);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Registry: react on file_name changes
|
||||
if (up.comfyClass === "KVLoadFromRegistry") {
|
||||
const fnW = W(up, "file_name");
|
||||
if (fnW && !fnW._kvtoolsHooked) {
|
||||
fnW._kvtoolsHooked = true;
|
||||
const orig = fnW.callback;
|
||||
fnW.callback = async () => {
|
||||
if (orig) orig();
|
||||
await loadRegistry();
|
||||
await syncKeyList(node); // pickNewKey prefers default_key
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------ Attach ------------------------------
|
||||
function isKVGet(node) { return node?.comfyClass === "KVGet"; }
|
||||
|
||||
function attachToKVGet(node) {
|
||||
if (!isKVGet(node)) return;
|
||||
if (node.__kvtools_attached) return;
|
||||
node.__kvtools_attached = true;
|
||||
|
||||
hideBuiltIns(node);
|
||||
ensureAsTypeDefault(node);
|
||||
buildOrEnsureCombo(node);
|
||||
ensurePreviewWidget(node);
|
||||
ensureRefreshButton(node);
|
||||
ensureDefaultKeyStorage(node);
|
||||
ensureDefaultButtons(node);
|
||||
ensureAutoRunToggle(node);
|
||||
|
||||
// Dropdown callback
|
||||
const ks = W(node, "key_select");
|
||||
if (ks && !ks.__kvtools_cb) {
|
||||
ks.__kvtools_cb = true;
|
||||
ks.callback = async () => {
|
||||
const keyW = W(node, "key");
|
||||
if (keyW) keyW.value = String(ks.value || "");
|
||||
node.setDirtyCanvas(true, true);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
};
|
||||
}
|
||||
|
||||
// Manual key changes
|
||||
const keyW = W(node, "key");
|
||||
if (keyW && !keyW.__kvtools_cb) {
|
||||
keyW.__kvtools_cb = true;
|
||||
const orig = keyW.callback;
|
||||
keyW.callback = async () => {
|
||||
if (orig) orig();
|
||||
const ks2 = W(node, "key_select");
|
||||
if (ks2 && ks2.options?.values?.includes(keyW.value)) ks2.value = keyW.value;
|
||||
node.setDirtyCanvas(true, true);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
};
|
||||
}
|
||||
|
||||
// Connection changes (e.g., new upstream)
|
||||
const origConn = node.onConnectionsChange?.bind(node);
|
||||
node.onConnectionsChange = async (...args) => {
|
||||
if (origConn) origConn(...args);
|
||||
hookUpstream(node);
|
||||
await loadRegistry();
|
||||
await syncKeyList(node);
|
||||
ensureAsTypeDefault(node);
|
||||
await updatePreview(node);
|
||||
if (W(node, "_kvtools_autorun")?.value) await tryQueueRun();
|
||||
};
|
||||
|
||||
// Initial after graph load (wait until upstream is connected)
|
||||
const wait = setInterval(async () => {
|
||||
const up = upstreamNode(node);
|
||||
if (up) {
|
||||
clearInterval(wait);
|
||||
hookUpstream(node);
|
||||
await loadRegistry();
|
||||
await syncKeyList(node);
|
||||
await updatePreview(node);
|
||||
// no autorun on initial attach
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ------------------------------ Register extension ------------------------------
|
||||
app.registerExtension({
|
||||
name: EXT_NAME,
|
||||
setup() {
|
||||
const ready = setInterval(() => {
|
||||
if (app.graph && Array.isArray(app.graph._nodes)) {
|
||||
clearInterval(ready);
|
||||
for (const n of app.graph._nodes) attachToKVGet(n);
|
||||
const origAdd = app.graph.add;
|
||||
app.graph.add = function(node) {
|
||||
const res = origAdd.call(this, node);
|
||||
attachToKVGet(node);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user