Add files via upload

This commit is contained in:
Pythza
2026-02-20 21:09:04 +01:00
committed by GitHub
parent 7313038d7f
commit 0bda2e1fb2
3 changed files with 8 additions and 164 deletions
+5 -21
View File
@@ -1,10 +1,9 @@
# ComfyUI-Pythza
A small collection of utility nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
A small utility node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI).
## Nodes
## 🧭 Node Navigator
### 🧭 Node Navigator
Pan the canvas to any node by its ID. Useful for large workflows where you need to jump between distant nodes quickly.
- Enter a **node ID** and click **"Go to node"**
@@ -13,36 +12,21 @@ Pan the canvas to any node by its ID. Useful for large workflows where you need
> **Tip:** You can find a node's ID by right-clicking it in ComfyUI — it's shown at the top of the context menu.
### 🎨 LoRA Formatter
Parses a LoRA tag string (e.g. `<lora:folder/MyModel:0.8>`) and displays a clean, readable preview.
- Strips common prefixes like `SDXL_`, `PONY_`, etc.
- Shows up to 3 LoRA blocks with labels (Custom, Slider, Detail)
- Live preview widget on the node itself
### 🔢 Count True Booleans
Counts how many of the connected boolean inputs are `True`. Accepts up to 20 optional boolean inputs.
Handy for conditional logic — e.g. "run this branch only if at least 3 options are enabled."
## Installation
### Manual
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/YOUR_USERNAME/ComfyUI-Pythza.git
git clone https://github.com/Pythza/ComfyUI-Pythza.git
```
Restart ComfyUI.
### ComfyUI Manager
Search for **Pythza** in the ComfyUI Manager install menu (once registered).
## Structure
```
ComfyUI-Pythza/
├── __init__.py # Node definitions (Python)
├── __init__.py # Node definition (Python)
├── js/
│ └── pythza.js # Frontend (canvas navigation + LoRA preview widget)
│ └── pythza.js # Frontend (canvas navigation)
└── README.md
```
+3 -110
View File
@@ -2,114 +2,11 @@
ComfyUI-Pythza — Utility nodes for ComfyUI
https://github.com/Pythza/ComfyUI-Pythza
"""
import re
WEB_DIRECTORY = "./js"
# ── Helpers ───────────────────────────────────────────────────────────────────
STRIP_PREFIXES = ["SDXL_PONY_", "PONY_SDXL_", "SDXL_", "PONY_", "_SDXL_", "_PONY_"]
BLOCK_LABELS = ["Custom", "Slider", "Detail"]
def strip_prefix(name):
for prefix in STRIP_PREFIXES:
if name.upper().startswith(prefix.upper()):
return name[len(prefix):]
return name
def parse_lora(entry, label):
entry = entry.strip()
if not entry:
return None
match = re.match(r'<lora:([^>]+)>', entry)
if not match:
return None
inner = match.group(1)
parts = inner.rsplit(":", 1)
if len(parts) != 2:
return None
path, strength = parts[0].strip(), parts[1].strip()
filename = path.replace("\\", "/").split("/")[-1]
filename = re.sub(r'\.safetensors$', '', filename, flags=re.IGNORECASE)
clean = strip_prefix(filename)
return f"{label}: {clean}\nStrength: {strength}"
def format_lora_string(lora_string):
top_level = []
depth = 0
current = []
for ch in lora_string:
if ch == "<":
depth += 1
elif ch == ">":
depth -= 1
if ch == "," and depth == 0:
top_level.append("".join(current).strip())
current = []
else:
current.append(ch)
top_level.append("".join(current).strip())
lines = []
for i, block in enumerate(top_level[:3]):
label = BLOCK_LABELS[i] if i < len(BLOCK_LABELS) else f"Block{i+1}"
parsed = parse_lora(block, label)
if parsed:
if lines:
lines.append("")
lines.append(parsed)
return "\n".join(lines) if lines else "(no LoRAs)"
# ── Node 1: LoRA Formatter ───────────────────────────────────────────────────
class PythzaLoraFormatter:
"""Parses a LoRA tag string and displays a clean formatted preview."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"lora_string": ("STRING", {"forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("formatted",)
FUNCTION = "format"
CATEGORY = "Pythza"
OUTPUT_NODE = True
def format(self, lora_string):
result = format_lora_string(lora_string)
return {"ui": {"text": [result]}, "result": (result,)}
# ── Node 2: Count True Booleans ──────────────────────────────────────────────
class PythzaCountTrue:
"""Counts how many of the connected boolean inputs are True."""
@classmethod
def INPUT_TYPES(cls):
optional = {f"bool_{i}": ("BOOLEAN", {"default": False}) for i in range(1, 21)}
return {"required": {}, "optional": optional}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("true_count",)
FUNCTION = "count"
CATEGORY = "Pythza"
def count(self, **kwargs):
return (sum(1 for v in kwargs.values() if v is True),)
# ── Node 3: Node Navigator ──────────────────────────────────────────────────
# ── Node Navigator ───────────────────────────────────────────────────────────
class PythzaNodeNavigator:
"""UI-only node with a button that pans the canvas to a target node by ID."""
@@ -134,15 +31,11 @@ class PythzaNodeNavigator:
# ── Registration ─────────────────────────────────────────────────────────────
NODE_CLASS_MAPPINGS = {
"PythzaLoraFormatter": PythzaLoraFormatter,
"PythzaCountTrue": PythzaCountTrue,
"PythzaNodeNavigator": PythzaNodeNavigator,
"PythzaNodeNavigator": PythzaNodeNavigator,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PythzaLoraFormatter": "LoRA Formatter (Pythza)",
"PythzaCountTrue": "Count True Booleans (Pythza)",
"PythzaNodeNavigator": "Node Navigator (Pythza)",
"PythzaNodeNavigator": "Node Navigator (Pythza)",
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
-33
View File
@@ -35,36 +35,3 @@ app.registerExtension({
};
}
});
// ── LoRA Formatter ──────────────────────────────────────────────────────────
app.registerExtension({
name: "Pythza.LoraFormatter",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "PythzaLoraFormatter") return;
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (origOnNodeCreated) origOnNodeCreated.apply(this, arguments);
const textWidget = this.addWidget("text", "preview", "", () => {}, {
multiline: true,
readonly: true,
});
textWidget.inputEl?.setAttribute("readonly", true);
this._previewWidget = textWidget;
this.setSize([300, 200]);
};
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
if (origOnExecuted) origOnExecuted.apply(this, arguments);
if (message?.text?.[0] !== undefined && this._previewWidget) {
this._previewWidget.value = message.text[0];
const lines = message.text[0].split("\n").length;
this.setSize([300, Math.max(120, 40 + lines * 20)]);
this.setDirtyCanvas(true);
}
};
}
});