Download utils/log.py from turtle89431/Moxie-Multimedia: direct link, hf CLI and curl.
- Browser
- Download file 7.27 kB
-
https://huggingface.co/turtle89431/Moxie-Multimedia/resolve/main/utils/log.py
- Command line
-
hf download hf://turtle89431/Moxie-Multimedia/utils/log.py
-
curl -L -o log.py https://huggingface.co/turtle89431/Moxie-Multimedia/resolve/main/utils/log.py
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 | |
| 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): | |
| 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: | |
| 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) | |