Files
cicalooo-ComfyUI-H3-PowerLo…/h3lora/server.py
T
doggeddalle 0fc822da8c Add auto-balance & adaLN modality control
Introduce auto-balance measurement and per-modality adaLN scaling for MiniMax H3.

- Add gain.py to measure LoRA perturbation (rel, factor) and cache results; expose measure_file/measure_state_dict.
- Add modality.py to scale adaLN rows by {video,text,audio} and describe/apply those changes.
- Add server route /h3_power_lora_stack/balance (h3lora/server.py) and register it in __init__.py to support the UI button.
- Integrate measurement and modality scaling into apply_stack and report LoRA rel/factor suggestions.
- Add H3AdalnModality node and adaln_modality input in nodes.py.
- Update web UI (web/h3_power_lora_stack.js) with auto-balance/restore buttons, server call, badges, and client-side behavior.
- Document features and rationale in README.md.

These changes keep measurements cached and never boost measured LoRAs (factor clamped ≤1).
2026-08-08 21:15:17 +10:00

92 lines
2.9 KiB
Python

"""HTTP route backing the node's auto-balance button.
The measurement has to happen server-side: it needs the LoRA files themselves,
which the browser has no access to. Results are cached in :mod:`.gain` on
(path, mtime, size), so re-pressing the button, or toggling a row, is free after
the first read of each file.
"""
from __future__ import annotations
import logging
from . import gain
LOG = logging.getLogger("h3.powerlorastack")
ROUTE = "/h3_power_lora_stack/balance"
def measure_names(names):
"""``[lora_name]`` -> ``{lora_name: {rel, factor, layers, note}}``.
Imported lazily by the route so the module stays importable without a
running server (the calibration scripts use it directly).
"""
from .nodes import _resolve_lora
out = {}
seen: dict[str, str] = {}
for name in names:
if not name or name == "None" or name in out:
continue
path = _resolve_lora(name)
if path is None:
out[name] = {"rel": None, "factor": 1.0, "layers": 0,
"note": "not found"}
continue
info = gain.measure_file(path)
entry = {
"rel": info.get("rel"),
"factor": float(info.get("factor", 1.0)),
"layers": info.get("layers", 0),
"ranks": info.get("ranks", []),
"note": "",
}
if info.get("error"):
entry["note"] = "unreadable"
elif not info.get("rel"):
entry["note"] = "no measurable layers"
else:
fp = info.get("fingerprint")
first = seen.get(fp) if fp else None
if first is not None:
entry["note"] = f"same adapter as {first}"
elif fp:
seen[fp] = name
out[name] = entry
return out
def register():
"""Attach the route to ComfyUI's aiohttp app, if there is one."""
try:
from aiohttp import web
from server import PromptServer
except Exception: # running outside ComfyUI
return False
instance = getattr(PromptServer, "instance", None)
if instance is None or not hasattr(instance, "routes"):
return False
@instance.routes.post(ROUTE)
async def _balance(request):
try:
body = await request.json()
except Exception:
return web.json_response({"error": "bad request"}, status=400)
names = body.get("loras")
if not isinstance(names, list):
return web.json_response({"error": "expected {'loras': [...]}"}, status=400)
try:
results = measure_names([n for n in names if isinstance(n, str)])
except Exception as exc:
LOG.exception("H3 Power LoRA Stack: balance failed")
return web.json_response({"error": str(exc)}, status=500)
return web.json_response({
"results": results,
"reference": gain.REFERENCE_REL,
})
return True