joeygambino commited on
Commit
7f18fed
·
verified ·
1 Parent(s): a4b567d

v1.4: LoRA Stack node (dropdown+strength UI, chainable), shipped wired into the loader

Browse files
INSTRUCTIONS.md CHANGED
@@ -128,17 +128,19 @@ is simpler.
128
 
129
  ## 7. LoRAs
130
 
131
- As of v1.4 the loader takes **multiple LoRAs**. Two ways, and they stack:
132
-
133
- - Pick one in the `lora_file` dropdown (loads at `lora_strength`), and/or
134
- - List any number in `lora_path`, separated by commas or newlines, each with
135
- an optional per-LoRA strength:
136
-
137
- my_style.safetensors@0.7, my_character.safetensors@0.9
138
- F:/somewhere/else/extra.safetensors
139
-
140
- Entries without a strength use the `lora_strength` widget. `@` and `:` both
141
- work as the strength separator (Windows drive letters are handled).
 
 
142
 
143
  All LoRAs are fused into the model at load, so there is no per-step cost.
144
  Note: LoRAs apply on the **safetensors DiT path** only — they are ignored when
 
128
 
129
  ## 7. LoRAs
130
 
131
+ As of v1.4 the loader takes **multiple LoRAs**, and the workflow ships with
132
+ the node for it: the **LoRA Stack** node, already wired into the Model Loader.
133
+
134
+ - Pick up to three LoRAs in its dropdowns, each with its own strength slider.
135
+ Slots left on `(none)` do nothing.
136
+ - Need more than three? Add another LoRA Stack node (Add Node > JoyAI-Echo >
137
+ JoyEcho LoRA Stack) and chain it: first node's `lora_stack` output into the
138
+ second node's `lora_stack` input, second node into the Model Loader.
139
+ - Everything stacks with the Model Loader's own `lora_file` pick.
140
+
141
+ Power users / automation: `lora_path` also accepts a text list — one entry per
142
+ line or comma-separated, each with an optional strength suffix
143
+ (`my_style.safetensors@0.7`). Same result, no extra nodes.
144
 
145
  All LoRAs are fused into the model at load, so there is no per-step cost.
146
  Note: LoRAs apply on the **safetensors DiT path** only — they are ignored when
__init__.py CHANGED
@@ -25,6 +25,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
25
  # ---------------------------------------------------------------- upstream JoyEcho nodes
26
  from .nodes import (
27
  JoyEcho_ModelLoader,
 
28
  JoyEcho_TextEncode,
29
  JoyEcho_Generate,
30
  JoyEcho_SingleShotGenerate,
@@ -35,6 +36,7 @@ from .nodes import (
35
 
36
  NODE_CLASS_MAPPINGS.update({
37
  "JoyEcho_ModelLoader": JoyEcho_ModelLoader,
 
38
  "JoyEcho_TextEncode": JoyEcho_TextEncode,
39
  "JoyEcho_Generate": JoyEcho_Generate,
40
  "JoyEcho_SingleShotGenerate": JoyEcho_SingleShotGenerate,
@@ -44,6 +46,7 @@ NODE_CLASS_MAPPINGS.update({
44
  })
45
  NODE_DISPLAY_NAME_MAPPINGS.update({
46
  "JoyEcho_ModelLoader": "JoyEcho Model Loader",
 
47
  "JoyEcho_TextEncode": "JoyEcho Text Encode",
48
  "JoyEcho_Generate": "JoyEcho Generate (Multi-Shot)",
49
  "JoyEcho_SingleShotGenerate": "JoyEcho Single Shot Generate",
 
25
  # ---------------------------------------------------------------- upstream JoyEcho nodes
26
  from .nodes import (
27
  JoyEcho_ModelLoader,
28
+ JoyEcho_LoraStacker,
29
  JoyEcho_TextEncode,
30
  JoyEcho_Generate,
31
  JoyEcho_SingleShotGenerate,
 
36
 
37
  NODE_CLASS_MAPPINGS.update({
38
  "JoyEcho_ModelLoader": JoyEcho_ModelLoader,
39
+ "JoyEcho_LoraStacker": JoyEcho_LoraStacker,
40
  "JoyEcho_TextEncode": JoyEcho_TextEncode,
41
  "JoyEcho_Generate": JoyEcho_Generate,
42
  "JoyEcho_SingleShotGenerate": JoyEcho_SingleShotGenerate,
 
46
  })
47
  NODE_DISPLAY_NAME_MAPPINGS.update({
48
  "JoyEcho_ModelLoader": "JoyEcho Model Loader",
49
+ "JoyEcho_LoraStacker": "JoyEcho LoRA Stack",
50
  "JoyEcho_TextEncode": "JoyEcho Text Encode",
51
  "JoyEcho_Generate": "JoyEcho Generate (Multi-Shot)",
52
  "JoyEcho_SingleShotGenerate": "JoyEcho Single Shot Generate",
nodes.py CHANGED
@@ -253,6 +253,41 @@ def _resolve_gemma_file(choice: str) -> str:
253
  return _resolve_cat_file(choice, _GEMMA_FILE_CATS, "gemma_file", _GEMMA_FILE_MANUAL)
254
 
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  class JoyEcho_ModelLoader:
257
  """Load JoyAI-Echo model components: text encoder, DiT generator, and VAEs."""
258
 
@@ -306,6 +341,11 @@ class JoyEcho_ModelLoader:
306
  "path (ignored when a GGUF DiT is selected). Refresh the node "
307
  "list (R) after adding files.",
308
  }),
 
 
 
 
 
309
  "lora_path": ("STRING", {
310
  "default": "",
311
  "multiline": True,
@@ -364,6 +404,7 @@ class JoyEcho_ModelLoader:
364
 
365
  def load_model(self, checkpoint_path: str = "", gemma_path: str = "",
366
  lora_path: str = "", lora_strength: float = 1.0,
 
367
  low_vram: bool = False, fp8_transformer: bool = False,
368
  model_file: str = _MODEL_FILE_MANUAL,
369
  lora_file: str = _LORA_FILE_MANUAL,
@@ -490,6 +531,8 @@ class JoyEcho_ModelLoader:
490
  if dropdown_lora:
491
  _lora_entries.append((dropdown_lora, float(lora_strength)))
492
  _lora_entries.extend(_parse_lora_entries(lora_path, lora_strength))
 
 
493
  # dedupe identical paths (dropdown pick repeated in lora_path)
494
  _seen = set()
495
  _lora_entries = [e for e in _lora_entries
 
253
  return _resolve_cat_file(choice, _GEMMA_FILE_CATS, "gemma_file", _GEMMA_FILE_MANUAL)
254
 
255
 
256
+ class JoyEcho_LoraStacker:
257
+ """Chainable LoRA stack - the familiar dropdown+strength UI. Each node adds
258
+ up to three LoRAs; wire lora_stack to another stacker to chain more, and
259
+ the final one into the Model Loader's lora_stack input. Every entry is
260
+ fused into the DiT at load (safetensors DiT path only)."""
261
+
262
+ @classmethod
263
+ def INPUT_TYPES(cls):
264
+ lora_list = ["(none)"] + [x for x in _list_lora_files()
265
+ if x != _LORA_FILE_MANUAL]
266
+ opt = {"lora_stack": ("JOYECHO_LORA_STACK", {
267
+ "tooltip": "Chain from another LoRA Stack node to add more slots."})}
268
+ for i in (1, 2, 3):
269
+ opt[f"lora_{i}"] = (lora_list, {
270
+ "default": "(none)",
271
+ "tooltip": "LoRA from models/loras. (none) = slot unused."})
272
+ opt[f"strength_{i}"] = ("FLOAT", {
273
+ "default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05})
274
+ return {"required": {}, "optional": opt}
275
+
276
+ RETURN_TYPES = ("JOYECHO_LORA_STACK",)
277
+ RETURN_NAMES = ("lora_stack",)
278
+ FUNCTION = "stack"
279
+ CATEGORY = "JoyAI-Echo"
280
+
281
+ def stack(self, lora_stack=None, **kw):
282
+ out = list(lora_stack) if lora_stack else []
283
+ for i in (1, 2, 3):
284
+ choice = kw.get(f"lora_{i}") or "(none)"
285
+ if choice != "(none)" and ": " in str(choice):
286
+ out.append((_resolve_lora_file(choice),
287
+ float(kw.get(f"strength_{i}", 1.0))))
288
+ return (out,)
289
+
290
+
291
  class JoyEcho_ModelLoader:
292
  """Load JoyAI-Echo model components: text encoder, DiT generator, and VAEs."""
293
 
 
341
  "path (ignored when a GGUF DiT is selected). Refresh the node "
342
  "list (R) after adding files.",
343
  }),
344
+ "lora_stack": ("JOYECHO_LORA_STACK", {
345
+ "tooltip": "Wire a JoyEcho LoRA Stack node here for the "
346
+ "dropdown+strength multi-LoRA UI. Stacks with "
347
+ "lora_file and lora_path entries.",
348
+ }),
349
  "lora_path": ("STRING", {
350
  "default": "",
351
  "multiline": True,
 
404
 
405
  def load_model(self, checkpoint_path: str = "", gemma_path: str = "",
406
  lora_path: str = "", lora_strength: float = 1.0,
407
+ lora_stack=None,
408
  low_vram: bool = False, fp8_transformer: bool = False,
409
  model_file: str = _MODEL_FILE_MANUAL,
410
  lora_file: str = _LORA_FILE_MANUAL,
 
531
  if dropdown_lora:
532
  _lora_entries.append((dropdown_lora, float(lora_strength)))
533
  _lora_entries.extend(_parse_lora_entries(lora_path, lora_strength))
534
+ if lora_stack:
535
+ _lora_entries.extend((str(p), float(st)) for p, st in lora_stack)
536
  # dedupe identical paths (dropdown pick repeated in lora_path)
537
  _seen = set()
538
  _lora_entries = [e for e in _lora_entries
workflow/JoyEcho_Multishot_Workflow_PUBLIC.json CHANGED
@@ -1,8 +1,8 @@
1
  {
2
  "id": "dabc6ca9-5fa6-414e-9cdd-33077930aa75",
3
  "revision": 0,
4
- "last_node_id": 64,
5
- "last_link_id": 118,
6
  "nodes": [
7
  {
8
  "id": 2,
@@ -1472,6 +1472,11 @@
1472
  "name": "low_vram"
1473
  },
1474
  "link": null
 
 
 
 
 
1475
  }
1476
  ],
1477
  "outputs": [
@@ -1506,6 +1511,50 @@
1506
  false,
1507
  false
1508
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1509
  }
1510
  ],
1511
  "links": [
@@ -1628,6 +1677,14 @@
1628
  2,
1629
  0,
1630
  "JOYECHO_MODEL"
 
 
 
 
 
 
 
 
1631
  ]
1632
  ],
1633
  "groups": [
 
1
  {
2
  "id": "dabc6ca9-5fa6-414e-9cdd-33077930aa75",
3
  "revision": 0,
4
+ "last_node_id": 65,
5
+ "last_link_id": 119,
6
  "nodes": [
7
  {
8
  "id": 2,
 
1472
  "name": "low_vram"
1473
  },
1474
  "link": null
1475
+ },
1476
+ {
1477
+ "name": "lora_stack",
1478
+ "type": "JOYECHO_LORA_STACK",
1479
+ "link": 119
1480
  }
1481
  ],
1482
  "outputs": [
 
1511
  false,
1512
  false
1513
  ]
1514
+ },
1515
+ {
1516
+ "id": 65,
1517
+ "type": "JoyEcho_LoraStacker",
1518
+ "pos": [
1519
+ 2470.3581267217646,
1520
+ 66.94214876033413
1521
+ ],
1522
+ "size": [
1523
+ 300,
1524
+ 250
1525
+ ],
1526
+ "flags": {},
1527
+ "order": 0,
1528
+ "mode": 0,
1529
+ "inputs": [
1530
+ {
1531
+ "name": "lora_stack",
1532
+ "type": "JOYECHO_LORA_STACK",
1533
+ "link": null
1534
+ }
1535
+ ],
1536
+ "outputs": [
1537
+ {
1538
+ "name": "lora_stack",
1539
+ "type": "JOYECHO_LORA_STACK",
1540
+ "links": [
1541
+ 119
1542
+ ],
1543
+ "slot_index": 0
1544
+ }
1545
+ ],
1546
+ "title": "LoRA Stack (pick up to 3; chain for more)",
1547
+ "properties": {
1548
+ "Node name for S&R": "JoyEcho_LoraStacker"
1549
+ },
1550
+ "widgets_values": [
1551
+ "(none)",
1552
+ 1.0,
1553
+ "(none)",
1554
+ 1.0,
1555
+ "(none)",
1556
+ 1.0
1557
+ ]
1558
  }
1559
  ],
1560
  "links": [
 
1677
  2,
1678
  0,
1679
  "JOYECHO_MODEL"
1680
+ ],
1681
+ [
1682
+ 119,
1683
+ 65,
1684
+ 0,
1685
+ 64,
1686
+ 11,
1687
+ "JOYECHO_LORA_STACK"
1688
  ]
1689
  ],
1690
  "groups": [