Quantization + a per-model attention backend read from ComfyUI's own registry, so only installed ones are offered. The LoRA loader now takes its own list and scores wrapper prefixes against the model's key map, instead of applying a diffusers LoRA as a silent no-op.
102 lines
3.7 KiB
Python
102 lines
3.7 KiB
Python
"""Declarative list inputs: one STRING widget holding a JSON array of rows.
|
|
|
|
ComfyUI has no repeatable input, so a node that needs N of something (text encoders,
|
|
LoRAs) carries ONE string widget whose value is a JSON array. The `funpack_list` entry
|
|
in that widget's options describes a row, so both frontends can render it as rows with
|
|
an Add button instead of raw JSON: the canvas widget in `web/funpack_list.js`, and the
|
|
FunPack Editor's node page (`movie_editor/frontend/models.js`).
|
|
|
|
The node itself never has to care which frontend wrote the value — `parse_rows` accepts
|
|
the JSON string, an already-parsed list, or a single row.
|
|
"""
|
|
import json
|
|
|
|
# Row field kinds a frontend must know how to draw.
|
|
FIELD_KINDS = {"combo", "string", "int", "float", "boolean"}
|
|
|
|
|
|
def field(name, kind, *, label=None, choices=None, default=None, min=None, max=None,
|
|
step=None, tooltip=None, width=None):
|
|
"""One column of a list row."""
|
|
spec = {"name": name, "kind": kind, "label": label or name}
|
|
if choices is not None:
|
|
spec["choices"] = list(choices)
|
|
if default is not None:
|
|
spec["default"] = default
|
|
for key, value in (("min", min), ("max", max), ("step", step),
|
|
("tooltip", tooltip), ("width", width)):
|
|
if value is not None:
|
|
spec[key] = value
|
|
return spec
|
|
|
|
|
|
def list_widget(item, fields, *, default=None, add_label=None, max_rows=0, tooltip=None):
|
|
"""An INPUT_TYPES entry for a list input. Returns the ("STRING", {...}) tuple."""
|
|
spec = {"item": item, "fields": list(fields), "add_label": add_label or f"+ Add {item}"}
|
|
if max_rows:
|
|
spec["max_rows"] = int(max_rows)
|
|
opts = {
|
|
"default": json.dumps(default or []),
|
|
"multiline": False,
|
|
"funpack_list": spec,
|
|
}
|
|
if tooltip:
|
|
opts["tooltip"] = tooltip
|
|
return ("STRING", opts)
|
|
|
|
|
|
def _coerce(value, kind, default=None):
|
|
if kind == "boolean":
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
return bool(value) if value is not None else bool(default)
|
|
if kind in ("int", "float"):
|
|
try:
|
|
number = float(value)
|
|
except (TypeError, ValueError):
|
|
number = float(default or 0)
|
|
if number != number or number in (float("inf"), float("-inf")):
|
|
number = float(default or 0)
|
|
return int(number) if kind == "int" else number
|
|
if value is None:
|
|
return default
|
|
return value
|
|
|
|
|
|
def parse_rows(value, fields, *, key=None, drop_empty=True):
|
|
"""Normalize a list widget's value into a list of row dicts.
|
|
|
|
`key` names the field a row is worthless without (the file name, usually); rows whose
|
|
key is missing, empty or "None" are dropped unless `drop_empty` is off. A row may also
|
|
carry "on": False to be skipped without deleting it.
|
|
"""
|
|
if isinstance(value, str):
|
|
try:
|
|
value = json.loads(value or "[]")
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
return []
|
|
if isinstance(value, dict):
|
|
value = [value]
|
|
if not isinstance(value, list):
|
|
return []
|
|
|
|
specs = {f["name"]: f for f in fields}
|
|
if key is None and fields:
|
|
key = fields[0]["name"]
|
|
|
|
rows = []
|
|
for index, raw in enumerate(value):
|
|
if not isinstance(raw, dict):
|
|
continue
|
|
if not _coerce(raw.get("on", True), "boolean", True):
|
|
continue
|
|
row = {"index": index}
|
|
for name, spec in specs.items():
|
|
row[name] = _coerce(raw.get(name), spec["kind"], spec.get("default"))
|
|
if drop_empty and key:
|
|
k = row.get(key)
|
|
if k is None or (isinstance(k, str) and k.strip() in ("", "None")):
|
|
continue
|
|
rows.append(row)
|
|
return rows
|