[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
[Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.
Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.
Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.
Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.
Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""Saying what happened, at a severity, from somewhere.
|
|
|
|
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
|
|
[Log] Alert: FunPack Sampler | no image is connected, starting from noise
|
|
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
|
|
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
|
|
|
|
Four levels, and the difference between the middle two is the one that matters:
|
|
|
|
* **Info** -- it worked, and here is what it actually did. Not decoration: "loaded
|
|
on GPU using sage attention" is how you find out the attention setting did
|
|
nothing, months before the pictures look wrong enough to investigate.
|
|
* **Alert** -- something is unset or unconnected and the run continues without
|
|
it. A choice you may not have made on purpose.
|
|
* **Warning** -- something will NOT do what its name says. This is the level v4
|
|
needed and did not have: 560 handlers swallowed a failure and carried on, so a
|
|
feature could be switched on, do nothing, and never say so.
|
|
* **Critical** -- the run cannot proceed.
|
|
|
|
Records are kept in a small ring as well as printed, so the app can show them.
|
|
The buffer is bounded because a long rental session must not turn a log into a
|
|
memory leak.
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
from collections import deque
|
|
from typing import Optional
|
|
|
|
LABEL = "[Log]"
|
|
|
|
INFO = "Info"
|
|
ALERT = "Alert"
|
|
WARNING = "Warning"
|
|
CRITICAL = "Critical"
|
|
|
|
LEVELS = (INFO, ALERT, WARNING, CRITICAL)
|
|
|
|
# Enough to cover a long generation and its aftermath, small enough to be free.
|
|
HISTORY = 500
|
|
_records: deque = deque(maxlen=HISTORY)
|
|
|
|
# Keys already said once. Cleared per run rather than per process: v4's dedup
|
|
# lasted the life of the interpreter, so a session reported the first generation
|
|
# that went inert and stayed quiet for every one after it.
|
|
_said: set = set()
|
|
|
|
|
|
def line(level: str, source: str, message: str) -> str:
|
|
"""The one place the format is decided."""
|
|
where = f"{source} | " if source else ""
|
|
return f"{LABEL} {level}: {where}{message}"
|
|
|
|
|
|
def record(level: str, source: str, message: str) -> dict:
|
|
entry = {"at": time.time(), "level": level, "source": source, "message": message}
|
|
_records.append(entry)
|
|
print(line(level, source, message), file=sys.stderr, flush=True)
|
|
return entry
|
|
|
|
|
|
def info(source: str, message: str) -> dict:
|
|
return record(INFO, source, message)
|
|
|
|
|
|
def alert(source: str, message: str) -> dict:
|
|
return record(ALERT, source, message)
|
|
|
|
|
|
def warning(source: str, message: str) -> dict:
|
|
return record(WARNING, source, message)
|
|
|
|
|
|
def critical(source: str, message: str) -> dict:
|
|
return record(CRITICAL, source, message)
|
|
|
|
|
|
def once(key: str, level: str, source: str, message: str) -> Optional[dict]:
|
|
"""Say it the first time only, until the next run.
|
|
|
|
For anything that would otherwise repeat every sampling step. Thirty copies
|
|
of one line say nothing the first did not, and burying the console is its own
|
|
way of hiding a failure.
|
|
"""
|
|
if key in _said:
|
|
return None
|
|
_said.add(key)
|
|
return record(level, source, message)
|
|
|
|
|
|
def failed(what: str, exc: BaseException) -> dict:
|
|
"""Something did not load. Always a Warning: it means a feature is absent."""
|
|
return warning(what, f"did not load -- {type(exc).__name__}: {exc}")
|
|
|
|
|
|
def new_run() -> None:
|
|
"""A generation is starting: whatever was said once may be said again."""
|
|
_said.clear()
|
|
|
|
|
|
def history(level: Optional[str] = None, limit: int = HISTORY) -> list:
|
|
"""Recent records, newest last. For the app, and for the modules page."""
|
|
items = [r for r in _records if level is None or r["level"] == level]
|
|
return items[-limit:]
|
|
|
|
|
|
def _reset() -> None:
|
|
"""Tests only."""
|
|
_records.clear()
|
|
_said.clear()
|