File size: 7,274 Bytes
3afd6d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""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)