From 0eaeabbbc46db1c2d39f626271d64db4a7ab9ea1 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:28:28 +0300 Subject: [PATCH] Serialize compact LoRA rows as a list --- model_management.py | 68 +++++++++++++++++++++++++++++-------- web/funpack_lora_weights.js | 45 +++++++++++++++++++++++- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/model_management.py b/model_management.py index 6e35e2e3..1665b237 100644 --- a/model_management.py +++ b/model_management.py @@ -159,6 +159,7 @@ class FunPackApplyLoraWeights: optional = FlexibleOptionalInputType( any_type, { + "lora_list": ("STRING", {"default": "[]", "multiline": False}), "lora_0": (loras, {"default": "None"}), "lora_0_type": (LORA_TYPES, {"default": "general"}), "lora_0_base_weight": ( @@ -194,6 +195,10 @@ class FunPackApplyLoraWeights: def IS_CHANGED(cls, **kwargs): return float("nan") + @classmethod + def VALIDATE_INPUTS(cls, lora_list=None, lora_0=None): + return True + def _load_suggestions(self, refinement_key, mode, prompt_key): path = refiner_state_path(refinement_key, mode) if not os.path.exists(path): @@ -215,7 +220,52 @@ class FunPackApplyLoraWeights: return suggestions, "refiner suggestions applied" + def _entry_from_row(self, index, row): + if not isinstance(row, dict): + return None + if not row.get("on", True): + return None + + name = row.get("lora", row.get("name", "None")) + if not name or name == "None": + return None + + lora_type = row.get("type", row.get("lora_type", "general")) + if lora_type not in LORA_TYPES: + lora_type = "general" + + return { + "slot": index, + "name": name, + "type": lora_type, + "id": lora_state_id(name, lora_type), + "base_model_weight": safe_float(row.get("strength", row.get("base_weight", 1.0))), + } + + def _iter_lora_list(self, value): + if value in (None, "", "[]"): + return None + if isinstance(value, str): + try: + value = json.loads(value) + except (json.JSONDecodeError, TypeError, ValueError): + return None + if not isinstance(value, list): + return None + + entries = [] + for index, row in enumerate(value): + entry = self._entry_from_row(index, row) + if entry: + entries.append(entry) + return entries + def _iter_slots(self, kwargs): + listed_entries = self._iter_lora_list(kwargs.get("lora_list")) + if listed_entries is not None: + yield from listed_entries + return + indexed_slots = set() for key in kwargs: match = re.fullmatch(r"lora_(\d+)", key) @@ -225,21 +275,9 @@ class FunPackApplyLoraWeights: for index in sorted(indexed_slots): lora_name = kwargs.get(f"lora_{index}", "None") if isinstance(lora_name, dict): - if not lora_name.get("on", True): - continue - name = lora_name.get("lora", "None") - if not name or name == "None": - continue - lora_type = lora_name.get("type", lora_name.get("lora_type", "general")) - if lora_type not in LORA_TYPES: - lora_type = "general" - yield { - "slot": index, - "name": name, - "type": lora_type, - "id": lora_state_id(name, lora_type), - "base_model_weight": safe_float(lora_name.get("strength", lora_name.get("base_weight", 1.0))), - } + entry = self._entry_from_row(index, lora_name) + if entry: + yield entry continue shifted_base_weight = None diff --git a/web/funpack_lora_weights.js b/web/funpack_lora_weights.js index 30d9158b..196e8bab 100644 --- a/web/funpack_lora_weights.js +++ b/web/funpack_lora_weights.js @@ -5,6 +5,7 @@ const NODE_NAME = "FunPackApplyLoraWeights"; const LORA_TYPES = ["general", "concept", "style", "quality", "character"]; const ADD_BUTTON_NAME = "+ Add LoRA"; const HEADER_WIDGET_NAME = "funpack_lora_header"; +const LORA_LIST_WIDGET_NAME = "lora_list"; const BASE_WIDGET_NAMES = new Set(["positive_prompt", "refinement_key", "mode", "per_block"]); let cachedLoraValues = null; @@ -108,7 +109,11 @@ function inside(pos, bounds) { } function getLoraValues(nodeData) { - return cachedLoraValues || nodeData?.input?.optional?.lora_0?.[0] || ["None"]; + const inputType = nodeData?.input?.optional?.lora_0?.[0]; + const embeddedValues = Array.isArray(inputType) + ? inputType + : nodeData?.input?.optional?.lora_0?.[1]?.available_loras; + return cachedLoraValues || (Array.isArray(embeddedValues) ? embeddedValues : ["None"]); } function rememberLoraValues(nodeData, values) { @@ -286,9 +291,28 @@ class FunPackLoraHeaderWidget extends FunPackBaseWidget { } } +class FunPackLoraListWidget extends FunPackBaseWidget { + constructor(node) { + super(LORA_LIST_WIDGET_NAME); + this.node = node; + this.value = "[]"; + } + + computeSize(width) { + return [width, 0]; + } + + draw() {} + + serializeValue() { + return JSON.stringify(getLoraRows(this.node).map((row) => row.serializeValue())); + } +} + class FunPackLoraRowWidget extends FunPackBaseWidget { constructor(name, value, nodeData) { super(name); + this.options.serialize = false; this.value = normalizeRowValue(value); this.nodeData = nodeData; this.draggedWeight = false; @@ -494,6 +518,7 @@ function isDefaultLoraWidget(widget) { function isManagedWidget(widget) { return ( isDefaultLoraWidget(widget) || + widget.name === LORA_LIST_WIDGET_NAME || widget.name === HEADER_WIDGET_NAME || widget.name === ADD_BUTTON_NAME || widget.__funpackLoraRow === true @@ -611,6 +636,23 @@ function parseSerializedRows(values, baseCount) { } const rowValues = values.slice(baseCount); + for (const value of rowValues) { + if (typeof value !== "string") { + continue; + } + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + const rows = parsed.map(normalizeRowValue); + if (rows.length) { + return rows; + } + } + } catch (_error) { + // Older workflows stored rows as individual widget values. + } + } + const objectRows = rowValues .filter((value) => value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "lora")) .map(normalizeRowValue); @@ -637,6 +679,7 @@ function ensureCompactLoraUi(node, nodeData, info = null) { const rows = serializedRows.length ? serializedRows : defaultRows; removeManagedWidgets(node); + node.addCustomWidget(new FunPackLoraListWidget(node)); node.addCustomWidget(new FunPackLoraHeaderWidget()); if (rows.length) { for (const row of rows) {