v1.4: LoRA Stack node (dropdown+strength UI, chainable), shipped wired into the loader
Browse files- INSTRUCTIONS.md +13 -11
- __init__.py +3 -0
- nodes.py +43 -0
- workflow/JoyEcho_Multishot_Workflow_PUBLIC.json +59 -2
INSTRUCTIONS.md
CHANGED
|
@@ -128,17 +128,19 @@ is simpler.
|
|
| 128 |
|
| 129 |
## 7. LoRAs
|
| 130 |
|
| 131 |
-
As of v1.4 the loader takes **multiple LoRAs**
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
| 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":
|
| 5 |
-
"last_link_id":
|
| 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": [
|