From b6edaf6db68ba13ad8304b4cd09cc9e776dd2f4c Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 16 May 2026 19:18:41 +0300 Subject: [PATCH] FunPackStudio: integrate full LoRA chain internally, remove lora_stack output LoRA pipeline is now fully internal to Studio: 1. FunPackApplyLoraWeights reads session weight suggestions for the current prompt and builds a stack with adjusted model weights 2. FunPackLoraLoader applies the stack to model and clip 3. refine_v2 receives the LoRA-patched model and applies the attn2 direction patch on top Removes the lora_stack return value - MODEL output already contains everything (LoRA patches + direction injection). External lora_stack input still accepted to bypass internal management entirely. Adds mode (ltx2/wan) and per_block settings to the LoRA tab in popup, stored in loras_config dict in studio_settings. Co-Authored-By: Claude Sonnet 4.6 --- conditioning.py | 98 +++++++++++++++++++++++++++++-------------- web/funpack_studio.js | 13 +++++- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/conditioning.py b/conditioning.py index 4f251b72..cceddbd4 100644 --- a/conditioning.py +++ b/conditioning.py @@ -11859,8 +11859,8 @@ class FunPackStudio: Advisor LLM, LoRA management, and Conditioning Adjust under one UI.""" CATEGORY = "FunPack" - RETURN_TYPES = ("CONDITIONING", "STRING", "STRING", "IMAGE", "STRING", "MODEL", "INT", "FUNPACK_LORA_STACK") - RETURN_NAMES = ("modified_positive", "status", "training_info", "loss_graph", "encoded_prompts", "model", "seed", "lora_stack") + RETURN_TYPES = ("CONDITIONING", "STRING", "STRING", "IMAGE", "STRING", "MODEL", "INT") + RETURN_NAMES = ("modified_positive", "status", "training_info", "loss_graph", "encoded_prompts", "model", "seed") FUNCTION = "run" DESCRIPTION = ( "FunPack Studio - all FunPack refinement tools in one node. " @@ -11868,6 +11868,7 @@ class FunPackStudio: ) _lora_loader = None + _apply_weights_node = None @classmethod def _get_lora_loader(cls): @@ -11882,6 +11883,19 @@ class FunPackStudio: cls._lora_loader = None return cls._lora_loader + @classmethod + def _get_apply_weights_node(cls): + if cls._apply_weights_node is None: + try: + try: + from .model_management import FunPackApplyLoraWeights + except ImportError: + from model_management import FunPackApplyLoraWeights + cls._apply_weights_node = FunPackApplyLoraWeights() + except Exception: + cls._apply_weights_node = None + return cls._apply_weights_node + @classmethod def INPUT_TYPES(cls): return { @@ -11909,7 +11923,7 @@ class FunPackStudio: "model": ("MODEL", {"tooltip": "Diffusion model. Required for LoRA loading and direction injection."}), "source_image": ("IMAGE",), "clip_vision_output": ("CLIP_VISION_OUTPUT",), - "lora_stack": ("FUNPACK_LORA_STACK", {"tooltip": "Optional external LoRA stack. Takes precedence over LoRAs configured inside Studio."}), + "lora_stack": ("FUNPACK_LORA_STACK", {"tooltip": "Optional external LoRA stack. Bypasses Studio's internal LoRA management entirely when connected."}), "refinement_key_input": ("STRING", {"forceInput": True}), "user_intent_prompt": ("STRING", {"multiline": True, "default": "", "forceInput": True}), "feedback_prompt": ("STRING", {"multiline": True, "default": "", "forceInput": True, @@ -11984,40 +11998,60 @@ class FunPackStudio: except Exception as e: print(f"[FunPackStudio] Advisor LLM load failed: {e}") - # --- LoRA stack --- - active_lora_stack = lora_stack + # --- LoRA: ApplyWeights (session suggestions) then Load --- + # 1. Build lora_list from studio settings or use provided external stack directly. + # 2. FunPackApplyLoraWeights reads session weight suggestions for current prompt. + # 3. FunPackLoraLoader applies the stack to model/clip. + # 4. refine_v2 then applies attn2 direction patch on top of the LoRA-patched model. + active_lora_stack = lora_stack # external stack bypasses steps 1-2 studio_loras = settings.get("loras", []) + lora_cfg = settings.get("loras_config", {}) if isinstance(settings.get("loras_config"), dict) else {} + lora_mode = str(lora_cfg.get("mode", "ltx2") or "ltx2") + per_block = bool(lora_cfg.get("per_block", False)) + if not active_lora_stack and studio_loras and model is not None: - lora_entries = [] - for idx, entry in enumerate(studio_loras if isinstance(studio_loras, list) else []): - if not isinstance(entry, dict): - continue - name = str(entry.get("name", "") or "").strip() - if not name or name == "None": - continue - mw = float(entry.get("model_weight", 1.0) or 1.0) - cw = float(entry.get("clip_weight", 1.0) or 1.0) - lora_entries.append({ - "slot": idx, - "name": name, - "type": str(entry.get("type", "general") or "general"), - "base_model_weight": mw, - "base_clip_weight": cw, - "model_weight": mw, - "clip_weight": cw, - }) - if lora_entries: - raw_stack = {"refinement_key": key, "loras": lora_entries, "per_block": False} - loader = self._get_lora_loader() - if loader is not None: + valid_loras = [ + e for e in (studio_loras if isinstance(studio_loras, list) else []) + if isinstance(e, dict) and str(e.get("name", "") or "").strip() not in ("", "None") + ] + if valid_loras: + # Build lora_list in the format FunPackApplyLoraWeights expects + clip_weights = {str(e.get("name", "")): float(e.get("clip_weight", 1.0) or 1.0) + for e in valid_loras} + lora_list_json = json.dumps([ + { + "name": str(e.get("name", "")), + "type": str(e.get("type", "general") or "general"), + "strength": float(e.get("model_weight", 1.0) or 1.0), + "on": True, + } + for e in valid_loras + ]) + apply_node = self._get_apply_weights_node() + if apply_node is not None: try: - model, clip, active_lora_stack, lora_status = loader.load_loras( - model, raw_stack, clip=clip + effective_key = key or "studio" + active_lora_stack, lora_apply_status = apply_node.apply_lora_weights( + active_prompt, effective_key, lora_mode, + per_block=per_block, + lora_list=lora_list_json, ) - print(f"[FunPackStudio] {lora_status}") + # Inject clip_weight values (ApplyLoraWeights only handles model weights) + for entry in active_lora_stack.get("loras", []): + entry["clip_weight"] = clip_weights.get(str(entry.get("name", "")), 1.0) + print(f"[FunPackStudio] {lora_apply_status}") + except Exception as e: + print(f"[FunPackStudio] Apply LoRA weights failed: {e}") + + loader = self._get_lora_loader() + if loader is not None and active_lora_stack is not None: + try: + model, clip, active_lora_stack, lora_load_status = loader.load_loras( + model, active_lora_stack, clip=clip + ) + print(f"[FunPackStudio] {lora_load_status}") except Exception as e: print(f"[FunPackStudio] LoRA load failed: {e}") - active_lora_stack = raw_stack # --- Refiner V2 --- refiner = FunPackVideoRefinerV2() @@ -12054,4 +12088,4 @@ class FunPackStudio: except Exception as e: status = f"{status}\nConditioning adjust failed: {e}" - return (cond, status, training_info, loss_graph, encoded_prompts, out_model, seed, active_lora_stack) + return (cond, status, training_info, loss_graph, encoded_prompts, out_model, seed) diff --git a/web/funpack_studio.js b/web/funpack_studio.js index d6871cad..90628454 100644 --- a/web/funpack_studio.js +++ b/web/funpack_studio.js @@ -64,6 +64,7 @@ function defaultSettings() { refiner: { mode: "Refine", advisor_mode: "Off", advisor_thinking: true, prompt_repair: true, im_feeling_lucky: false, reset_session: false, feedback_prompt: "", user_intent_prompt_override: "" }, advisor_llm: { enabled: false, model_path: "huihui-ai/Huihui-Qwen3-8B-abliterated-v2", dtype: "bfloat16" }, loras: [], + loras_config: { mode: "ltx2", per_block: false }, }; } @@ -507,9 +508,17 @@ function openPanel(node) { // LORA ───────────────────────────────────────────────────────────────────── function renderLora() { - body.append(sectionTitle("LoRA List")); + body.append(sectionTitle("LoRA Settings")); + if (!settings.loras_config) settings.loras_config = { mode: "ltx2", per_block: false }; + const modeSelect = selectEl(["ltx2", "wan"], settings.loras_config.mode); + modeSelect.addEventListener("change", () => { settings.loras_config.mode = modeSelect.value; }); + body.append(row("Model type", modeSelect)); + const perBlockToggle = toggleEl(settings.loras_config.per_block, "Per-block application"); + perBlockToggle.inp.addEventListener("change", () => { settings.loras_config.per_block = perBlockToggle.inp.checked; }); + body.append(row("Per-block", perBlockToggle.wrap)); body.append(el("div", "funpack-studio-hint", - "LoRAs are loaded in order each run. An external lora_stack input overrides this list entirely.")); + "Studio applies LoRAs internally: session weight suggestions are read first, then LoRAs are loaded, then the direction patch is applied on top. An external lora_stack input bypasses this entirely.")); + body.append(sectionTitle("LoRA List")); const list = el("div", "funpack-studio-lora-list");