turtle89431's picture
Upload folder using huggingface_hub (part 2)
3afd6d6 verified
Raw History Blame Contribute Delete
7.27 kB
"""Console logging helpers shared by Moxie Multimedia nodes."""
from __future__ import annotations
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from functools import wraps
import inspect
import logging
from typing import Any
from time import perf_counter
_BOLD_CYAN = "\033[1m\033[36m"
_RESET = "\033[0m"
__all__ = [
"log_node_info", "log_stage_time", "synchronize_execution_device",
"instrument_node_timing",
]
def log_node_info(node_name: str, message: str | None = None) -> None:
"""Display an informational node message in the ComfyUI console."""
title = node_name.replace(" (EasyMedia)", "")
suffix = f":{_RESET} {message}" if message is not None else _RESET
print(f"{_BOLD_CYAN}[Moxie] {title}{suffix}", flush=True) # noqa: T201
@contextmanager
def log_stage_time(
node_name: str,
stage: str,
*,
synchronize: Callable[[], None] | None = None,
) -> Iterator[None]:
"""Measure actual work, excluding upstream execution and prior GPU work."""
if synchronize is not None:
synchronize()
started = perf_counter()
try:
yield
if synchronize is not None:
synchronize()
except BaseException:
log_node_info(node_name, f"Timing | {stage} | failed after {perf_counter() - started:.3f} s")
raise
else:
log_node_info(node_name, f"Timing | {stage} | {perf_counter() - started:.3f} s")
def synchronize_execution_device() -> None:
"""Wait on the active ComfyUI device so GPU dispatch is not timed as work."""
import torch
from comfy import model_management
device = model_management.get_torch_device()
if device.type == "mps":
torch.mps.synchronize()
elif device.type == "cuda":
torch.cuda.synchronize(device)
elif device.type == "xpu":
torch.xpu.synchronize(device)
def _current_timing_label() -> str | None:
from comfy_execution.utils import get_executing_context
context = get_executing_context()
if context is None:
return None
from comfy_execution.progress import get_progress_state
progress = get_progress_state()
if progress.prompt_id != context.prompt_id or not progress.dynprompt.has_node(context.node_id):
return None
metadata = progress.dynprompt.get_node(context.node_id).get("_meta", {})
label = metadata.get("easy_media_timing") if isinstance(metadata, dict) else None
return label if isinstance(label, str) and label else None
def instrument_node_timing(node_class: type) -> None:
"""Wrap a native method once; only runtime-tagged project nodes are timed.
Preserve the descriptor and forward the actual cls/self so V3 execution
clones keep their hidden inputs. No execution-engine hooks or node registry
replacements are required; timing metadata lives only in the dynamic graph.
"""
method_name = "execute" if callable(getattr(node_class, "execute", None)) else node_class.FUNCTION
descriptor = inspect.getattr_static(node_class, method_name)
original = descriptor.__func__ if isinstance(descriptor, (classmethod, staticmethod)) else descriptor
if getattr(original, "__easy_media_timed__", False):
return
if inspect.iscoroutinefunction(original):
@wraps(original)
async def timed(*args: Any, **kwargs: Any) -> Any:
label = _current_timing_label()
if label is None:
return await original(*args, **kwargs)
with log_stage_time("MultiTrack Project", label, synchronize=synchronize_execution_device):
return await original(*args, **kwargs)
else:
@wraps(original)
def timed(*args: Any, **kwargs: Any) -> Any:
label = _current_timing_label()
if label is None:
return original(*args, **kwargs)
with log_stage_time("MultiTrack Project", label, synchronize=synchronize_execution_device):
return original(*args, **kwargs)
timed.__easy_media_timed__ = True
if isinstance(descriptor, classmethod):
replacement = classmethod(timed)
elif isinstance(descriptor, staticmethod):
replacement = staticmethod(timed)
else:
replacement = timed
setattr(node_class, method_name, replacement)
# ---------------------------------------------------------------------------
# Console privacy scrub
#
# The pack presents itself as "Moxie Multimedia Suite" and must not leak the
# underlying model/vendor names through console output. The main offender is
# ComfyUI core itself (e.g. model_patcher logs "Model MiniMaxH3 prepared for
# dynamic VRAM loading ..." using the detected architecture class name).
#
# The filter below rewrites those tokens in DISPLAYED text only (record.msg,
# and pre-rendered tracebacks when they contain a token); no execution logic,
# node registration or workflow behaviour is affected.
# ---------------------------------------------------------------------------
# The cloud-API host must stay readable for provider debugging.
_SCRUB_PROTECT = ("minimaxi.com", "\x00MOXIEPROTECT\x00")
_SCRUB_PAIRS = (
("MiniMaxH3", "Moxie"),
("MiniMax H3", "Moxie"),
("MINIMAX_H3", "MOXIE"),
("minimax_h3", "moxie"),
("MiniMax", "Moxie"),
("MINIMAX", "MOXIE"),
("minimax", "moxie"),
)
_SCRUB_TOKENS = tuple(old for old, _ in _SCRUB_PAIRS)
def _scrub_text(text: str) -> str:
protected, sentinel = _SCRUB_PROTECT
text = text.replace(protected, sentinel)
for old, new in _SCRUB_PAIRS:
text = text.replace(old, new)
return text.replace(sentinel, protected)
class _ConsolePrivacyFilter(logging.Filter):
"""Rewrite internal model/vendor tokens before records reach the console."""
def filter(self, record: logging.LogRecord) -> bool: # noqa: A003
try:
message = record.getMessage()
except Exception:
message = None
if isinstance(message, str):
scrubbed = _scrub_text(message)
if scrubbed != message:
record.msg = scrubbed
record.args = None
if record.exc_info is not None and record.exc_text is None:
import traceback
try:
rendered = "".join(traceback.format_exception(*record.exc_info))
except Exception:
rendered = None
if rendered is not None and any(token in rendered for token in _SCRUB_TOKENS):
record.exc_text = _scrub_text(rendered)
record.exc_info = None
return True
_privacy_filter_installed = False
def install_console_privacy_filter() -> None:
"""Attach the console scrub to the root logger and its current handlers.
Idempotent. ComfyUI installs its own root handlers during startup, before
custom node packs are imported, so attaching here covers everything the
user sees in the console afterwards."""
global _privacy_filter_installed
if _privacy_filter_installed:
return
_privacy_filter_installed = True
root = logging.getLogger()
scrubber = _ConsolePrivacyFilter()
root.addFilter(scrubber)
for handler in list(root.handlers):
handler.addFilter(scrubber)