* feat(history): normalize bounded execution evidence (#209) * fix(history): bound lifecycle kind checks before hashing (#209) * feat(history): persist execution attempts with restore fencing (#209) * test(api): bootstrap standalone backup restore fixtures (#209) * feat(history): isolate bounded asynchronous ledger writes (#209) * feat(history): observe actual queue attempts without altering execution (#209) * fix(database): honor package-scoped configuration (#211) * test(history): exercise real cached and headless executions (#209) * fix(history): reject unsupported uniqueness and isolate package fixtures (#209) * test(history): support Python 3.10 package fixture cleanup (#209)
200 lines
6.8 KiB
Python
200 lines
6.8 KiB
Python
"""
|
|
ComfyUI_PromptManager: A comprehensive ComfyUI custom node that extends text encoding
|
|
with persistent prompt storage, advanced search capabilities, and an automatic image
|
|
gallery system.
|
|
|
|
This module provides prompt node types:
|
|
- PromptManager: CLIP encoding node that outputs CONDITIONING
|
|
- PromptManagerFocused: Editor-first layout using the same encoding contract
|
|
- PromptManagerSplit: Side-by-side editor and library using the same contract
|
|
- PromptManagerText: Text-only node that outputs STRING with prepend/append
|
|
functionality
|
|
|
|
The nodes share the same database backend for persistent prompt storage and include
|
|
an automatic image gallery system that monitors ComfyUI output directories and links
|
|
generated images to their source prompts.
|
|
|
|
Features:
|
|
- SQLite-based persistent prompt storage with deduplication
|
|
- Advanced search and categorization system
|
|
- Real-time image gallery with metadata extraction
|
|
- Web-based admin dashboard for prompt management
|
|
- Comprehensive logging and diagnostics
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
def get_version():
|
|
"""Parse version from pyproject.toml"""
|
|
try:
|
|
pyproject_path = Path(__file__).parent / "pyproject.toml"
|
|
if pyproject_path.exists():
|
|
content = pyproject_path.read_text()
|
|
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
if match:
|
|
return match.group(1)
|
|
except Exception:
|
|
pass
|
|
return "unknown"
|
|
|
|
|
|
from .prompt_manager import ( # noqa: E402
|
|
PromptManager,
|
|
PromptManagerFocused,
|
|
PromptManagerSplit,
|
|
)
|
|
from .prompt_manager_pair import PromptManagerPair # noqa: E402
|
|
from .prompt_manager_text import PromptManagerText # noqa: E402
|
|
from .prompt_search_list import PromptSearchList # noqa: E402
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"PromptManager": PromptManager,
|
|
"PromptManagerFocused": PromptManagerFocused,
|
|
"PromptManagerSplit": PromptManagerSplit,
|
|
"PromptManagerPair": PromptManagerPair,
|
|
"PromptManagerText": PromptManagerText,
|
|
"PromptSearchList": PromptSearchList,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"PromptManager": "Prompt Manager (Classic)",
|
|
"PromptManagerFocused": "Prompt Manager (Focused)",
|
|
"PromptManagerSplit": "Prompt Manager (Split)",
|
|
"PromptManagerPair": "Prompt Manager (+/-)",
|
|
"PromptManagerText": "Prompt Manager Text",
|
|
"PromptSearchList": "Prompt Search List",
|
|
}
|
|
|
|
# Canvas extension only. ComfyUI loads every .js under this directory into the
|
|
# canvas page, so the admin/gallery bundles and vendored libraries under web/
|
|
# are served by our own routes instead (see py/api).
|
|
WEB_DIRECTORY = "web/comfy"
|
|
|
|
# Add API routes (same pattern as ComfyUI_Assets)
|
|
try:
|
|
# Set extension URI
|
|
import os
|
|
|
|
from .py import config
|
|
from .py.api import PromptManagerAPI
|
|
|
|
extension_uri = os.path.dirname(__file__)
|
|
config.extension_uri = extension_uri
|
|
|
|
# Register API routes using the same pattern as ComfyUI_Assets
|
|
routes = config.routes
|
|
api = PromptManagerAPI()
|
|
api.add_routes(routes)
|
|
|
|
except Exception as e:
|
|
# Log to internal logging system without console spam
|
|
try:
|
|
from .utils.logging_config import get_logger
|
|
|
|
logger = get_logger("prompt_manager.init")
|
|
logger.error(f"Failed to register API routes: {e}")
|
|
except Exception:
|
|
pass
|
|
|
|
# Shared database, set up with the monitoring system below. Stays None when that
|
|
# fails so the queue hook can report the database as unavailable instead of crashing.
|
|
_global_db = None
|
|
|
|
# Start image monitoring globally at module import time
|
|
# This ensures images are linked regardless of which node is used in the workflow
|
|
try:
|
|
from .database.operations import PromptDatabase
|
|
from .utils.image_monitor import get_image_monitor
|
|
from .utils.prompt_tracker import get_prompt_tracker
|
|
from .utils.logging_config import get_logger
|
|
|
|
_init_logger = get_logger("prompt_manager.init")
|
|
_init_logger.info("Starting global image monitoring system...")
|
|
|
|
# Initialize database and get singleton instances
|
|
_global_db = PromptDatabase()
|
|
_global_prompt_tracker = get_prompt_tracker(_global_db)
|
|
_global_image_monitor = get_image_monitor(_global_db, _global_prompt_tracker)
|
|
|
|
# Start monitoring
|
|
_global_image_monitor.start_monitoring()
|
|
_init_logger.info("Global image monitoring system started successfully")
|
|
|
|
except Exception as e:
|
|
try:
|
|
from .utils.logging_config import get_logger
|
|
|
|
_init_logger = get_logger("prompt_manager.init")
|
|
_init_logger.error(f"Failed to start image monitoring: {e}")
|
|
except Exception:
|
|
print(f"[ComfyUI-PromptManager] Warning: Failed to start image monitoring: {e}")
|
|
|
|
|
|
def _db_factory():
|
|
"""The shared database for the queue hook; called lazily per request."""
|
|
if _global_db is None:
|
|
raise RuntimeError("PromptManager database unavailable")
|
|
return _global_db
|
|
|
|
|
|
# Count prompt runs when ComfyUI dequeues workflows (it skips unchanged nodes, so
|
|
# node execution alone misses re-runs). Separate from monitoring so either can fail.
|
|
try:
|
|
from .py.config import server_instance
|
|
from .utils.usage_tracking import register_queue_hook
|
|
|
|
register_queue_hook(server_instance, _db_factory)
|
|
except Exception as e:
|
|
try:
|
|
from .utils.logging_config import get_logger
|
|
|
|
get_logger("prompt_manager.init").error(f"Failed to register usage hook: {e}")
|
|
except Exception:
|
|
import logging
|
|
|
|
logging.getLogger("prompt_manager.init").error(
|
|
f"Failed to register usage hook: {e}"
|
|
)
|
|
|
|
|
|
def _history_db_factory() -> "PromptDatabase":
|
|
"""Initialize history on its worker even if monitoring initialization failed."""
|
|
if _global_db is not None:
|
|
return _global_db
|
|
from .database.operations import PromptDatabase
|
|
|
|
return PromptDatabase()
|
|
|
|
|
|
_history_registration = None
|
|
try:
|
|
from .py.config import server_instance
|
|
from .utils.execution_history import register_history_hook
|
|
|
|
_history_registration = register_history_hook(server_instance, _history_db_factory)
|
|
except Exception:
|
|
try:
|
|
from .utils.logging_config import get_logger
|
|
|
|
get_logger("prompt_manager.init").error("Failed to register execution history")
|
|
except Exception:
|
|
import logging
|
|
|
|
logging.getLogger("prompt_manager.init").error(
|
|
"Failed to register execution history"
|
|
)
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
|
|
|
# Print startup message with loaded tools
|
|
print()
|
|
print(f"\033[94m[ComfyUI-PromptManager] Version:\033[0m {get_version()}")
|
|
for node_key, display_name in NODE_DISPLAY_NAME_MAPPINGS.items():
|
|
# ASCII only: a Windows console using a legacy code page cannot print emoji,
|
|
# and a UnicodeEncodeError here would abort loading the whole node.
|
|
print(f" \033[94mLoaded:\033[0m {display_name}")
|
|
print(f"\033[94mTotal: {len(NODE_CLASS_MAPPINGS)} tools loaded\033[0m")
|
|
print()
|