Files
MajoorWaldi-ComfyUI-Majoor-…/__init__.py
T
Ewald db88ad982a feat: add MajoorSaveImage & MajoorSaveVideo nodes, generation_time_ms extraction, topbar MFV button announcement
- nodes.py: MajoorSaveImage (PNG with generation_time_ms in text chunks) and MajoorSaveVideo (MP4 via PyAV, GIF, WebP with metadata persistence)
- extractors_png.py: read generation_time_ms from PNG text chunks
- extractors.py: extract generation_time_ms from merged EXIF data into metadata dict
- extractors_video.py: extract generation_time_ms from ffprobe format tags
- messageCenter.js: add generation-time-persistence tip and topbar MFV button announcement
- CHANGELOG.md: document topbar MFV button and custom nodes
- README.md: add Custom Nodes section with input tables
- docs/CUSTOM_NODES.md: full node reference and extraction pipeline docs
- docs/DOCUMENTATION_INDEX.md: add CUSTOM_NODES.md entry
2026-04-12 19:16:07 +02:00

218 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ComfyUI-Majoor-AssetsManager
Advanced asset browser for ComfyUI with ratings, tags, and metadata management.
"""
from __future__ import annotations
import importlib
import logging
import os
import re
import sys
from pathlib import Path
from typing import Any
# ComfyUI extension metadata – populated from nodes.py below.
NODE_CLASS_MAPPINGS: dict[str, Any] = {}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {}
try:
from .nodes import (
NODE_CLASS_MAPPINGS as _ncm,
)
from .nodes import (
NODE_DISPLAY_NAME_MAPPINGS as _ndnm,
)
NODE_CLASS_MAPPINGS.update(_ncm)
NODE_DISPLAY_NAME_MAPPINGS.update(_ndnm)
except Exception:
logging.getLogger("majoor_assets_manager").debug(
"failed to import custom nodes", exc_info=True,
)
# Resolved below after `root` is known; keep None as sentinel so imports
# executed before root assignment don't accidentally use a relative path.
WEB_DIRECTORY = None
_logger = logging.getLogger("majoor_assets_manager")
_REGISTRY_HOOKS_DONE = False
_REGISTERED_APPS: set[int] = set()
def _read_version_from_pyproject() -> str:
try:
root = Path(__file__).resolve().parent
pyproject_path = root / "pyproject.toml"
if not pyproject_path.exists():
return "0.0.0"
raw = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"(.*?)"', raw, flags=re.MULTILINE)
if match:
return match.group(1).strip()
except Exception:
pass
return "0.0.0"
def _detect_branch_from_env() -> str:
candidates = (
"MAJOR_ASSETS_MANAGER_BRANCH",
"MAJOOR_ASSETS_MANAGER_BRANCH",
)
for key in candidates:
value = os.environ.get(key)
if value:
return value.strip()
return "main"
__version__ = _read_version_from_pyproject()
__branch__ = _detect_branch_from_env()
root = Path(__file__).resolve().parent
if WEB_DIRECTORY is None:
# js_dist/ contains the Vite-built bundle (npm run build).
# Fall back to js/ (raw source) when the dist directory has not been built yet
# so development without a build step still works.
_js_dist = root / "js_dist"
WEB_DIRECTORY = str(_js_dist if _js_dist.is_dir() else root / "js")
# Ensure extension-local packages (e.g. `mjr_am_backend`) are importable even when
# ComfyUI loads this module by file path without adding custom node root to sys.path.
try:
root_str = str(root)
if root_str not in sys.path:
sys.path.append(root_str)
except Exception:
_logger.debug("failed to ensure extension root on sys.path", exc_info=True)
def init_prompt_server() -> None:
"""
Initialize routes against ComfyUI PromptServer.
This is kept as a convenience wrapper for ComfyUI runtime.
Bootstrap stages are recorded in ``mjr_am_backend.bootstrap_report``
so their status is visible via ``/mjr/am/health``.
"""
global _REGISTRY_HOOKS_DONE
try:
from mjr_am_backend.bootstrap_report import (
record_stage,
)
except Exception:
record_stage = None # type: ignore[assignment]
def _record(name: str, status: str, severity: str = "none", detail: str | None = None) -> None:
if callable(record_stage):
try:
record_stage(name, status, severity, detail)
except Exception:
pass
try:
try:
from mjr_am_backend.runtime_activity import ensure_prompt_lifecycle_provider_registered
ensure_prompt_lifecycle_provider_registered()
_record("prompt_lifecycle", "ok")
except Exception as exc:
_logger.debug("failed to register prompt lifecycle provider", exc_info=True)
_record("prompt_lifecycle", "degraded", "internal", str(exc)[:120])
_registry = importlib.import_module("mjr_am_backend.routes.registry")
register_all_routes = getattr(_registry, "register_all_routes", None)
register_routes = getattr(_registry, "register_routes", None)
install_observability = getattr(_registry, "_install_observability_on_prompt_server", None)
# Always register decorator-backed routes first; this does not require app.
if not _REGISTRY_HOOKS_DONE and callable(register_all_routes):
register_all_routes()
_record("route_table", "ok")
from server import PromptServer # type: ignore
prompt_server = getattr(PromptServer, "instance", None)
app = getattr(prompt_server, "app", None)
user_manager = getattr(prompt_server, "user_manager", None)
if app is None:
_logger.debug("PromptServer app is not available yet; route table registered only")
_record("app_mount", "degraded", "internal", "PromptServer app not yet available")
return
if not _REGISTRY_HOOKS_DONE and callable(install_observability):
try:
install_observability()
_record("observability", "ok")
except Exception as exc:
_record("observability", "degraded", "internal", str(exc)[:120])
_REGISTRY_HOOKS_DONE = True
if callable(register_routes):
app_id = id(app)
if app_id not in _REGISTERED_APPS:
register_routes(app, user_manager=user_manager)
_REGISTERED_APPS.add(app_id)
_record("routes", "ok")
except Exception as exc:
_logger.exception("failed to initialize prompt server routes")
_record("init", "fatal", "fatal", str(exc)[:120])
def init(app) -> None:
"""Public explicit initializer for host integrations and tests."""
if app is None:
raise ValueError("init(app) requires a valid aiohttp app instance")
try:
from mjr_am_backend.routes.registry import register_routes
user_manager = None
try:
from server import PromptServer # type: ignore
user_manager = getattr(getattr(PromptServer, "instance", None), "user_manager", None)
except Exception:
user_manager = None
app_id = id(app)
if app_id not in _REGISTERED_APPS:
register_routes(app, user_manager=user_manager)
_REGISTERED_APPS.add(app_id)
except Exception:
_logger.exception("failed to initialize routes on provided app")
def register(app, user_manager=None) -> None:
"""
Explicit host integration entrypoint.
Args:
app: aiohttp application instance
user_manager: optional ComfyUI user manager for auth-aware route policies
"""
if app is None:
raise ValueError("register(app, user_manager=None) requires a valid aiohttp app instance")
try:
from mjr_am_backend.routes.registry import register_routes
app_id = id(app)
if app_id not in _REGISTERED_APPS:
register_routes(app, user_manager=user_manager)
_REGISTERED_APPS.add(app_id)
except Exception:
_logger.exception("failed to register routes on provided app")
try:
# Minimal ComfyUI custom_nodes import-time behavior:
# register routes/UI via explicit init path, no sys.modules mutation.
init_prompt_server()
except Exception:
_logger.exception("failed to initialize at import time")
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
"WEB_DIRECTORY",
"__version__",
"__branch__",
"init",
"register",
"init_prompt_server",
]