diff --git a/__init__.py b/__init__.py index b92d1b5..bc93503 100644 --- a/__init__.py +++ b/__init__.py @@ -82,6 +82,34 @@ except Exception as e: except: pass +# 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: + print(f"[ComfyUI-PromptManager] Warning: Failed to start image monitoring: {e}") + __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] # Print startup message with loaded tools diff --git a/database/models.py b/database/models.py index 71946ba..e14541a 100644 --- a/database/models.py +++ b/database/models.py @@ -91,9 +91,13 @@ class PromptModel: workflow_data TEXT, prompt_metadata TEXT, parameters TEXT, - FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE + FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE, + UNIQUE(prompt_id, filename) ) """) + + # Add unique constraint to existing databases (migration) + self._migrate_add_unique_constraint(conn) # Check if we need to migrate from old schema with workflow_name self._migrate_workflow_name_removal(conn) @@ -249,10 +253,100 @@ class PromptModel: self.logger.error(f"Foreign key migration error: {e}") # If migration fails, continue with existing schema + def _migrate_add_unique_constraint(self, conn: sqlite3.Connection) -> None: + """ + Add UNIQUE constraint on (prompt_id, filename) to prevent duplicate image entries. + + This migration: + 1. Checks if the constraint already exists + 2. Removes duplicate entries (keeping the most recent) + 3. Recreates the table with the UNIQUE constraint + + Args: + conn: Active database connection + """ + try: + # Check if the unique constraint already exists by looking at table info + cursor = conn.execute("PRAGMA index_list(generated_images)") + indexes = cursor.fetchall() + + # Check if we have a unique index on prompt_id, filename + has_unique_constraint = False + for idx in indexes: + if idx[2] == 1: # unique flag + cursor = conn.execute(f"PRAGMA index_info({idx[1]})") + columns = [col[2] for col in cursor.fetchall()] + if 'prompt_id' in columns and 'filename' in columns: + has_unique_constraint = True + break + + if has_unique_constraint: + return # Already migrated + + self.logger.info("Migrating database: adding UNIQUE constraint on (prompt_id, filename)") + + # First, remove duplicates keeping only the most recent (highest id) + conn.execute(""" + DELETE FROM generated_images + WHERE id NOT IN ( + SELECT MAX(id) FROM generated_images + GROUP BY prompt_id, filename + ) + """) + + duplicates_removed = conn.total_changes + if duplicates_removed > 0: + self.logger.info(f"Removed {duplicates_removed} duplicate image entries") + + # Create new table with UNIQUE constraint + conn.execute(""" + CREATE TABLE IF NOT EXISTS generated_images_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prompt_id INTEGER NOT NULL, + image_path TEXT NOT NULL, + filename TEXT NOT NULL, + generation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + file_size INTEGER, + width INTEGER, + height INTEGER, + format TEXT, + workflow_data TEXT, + prompt_metadata TEXT, + parameters TEXT, + FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE, + UNIQUE(prompt_id, filename) + ) + """) + + # Copy data + conn.execute(""" + INSERT INTO generated_images_new + (id, prompt_id, image_path, filename, generation_time, file_size, + width, height, format, workflow_data, prompt_metadata, parameters) + SELECT id, prompt_id, image_path, filename, generation_time, file_size, + width, height, format, workflow_data, prompt_metadata, parameters + FROM generated_images + """) + + # Drop old table and rename new one + conn.execute("DROP TABLE generated_images") + conn.execute("ALTER TABLE generated_images_new RENAME TO generated_images") + + # Recreate indexes + conn.execute("CREATE INDEX IF NOT EXISTS idx_prompt_images ON generated_images(prompt_id)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_image_path ON generated_images(image_path)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_generation_time ON generated_images(generation_time)") + + self.logger.info("UNIQUE constraint migration completed successfully") + + except Exception as e: + self.logger.error(f"UNIQUE constraint migration error: {e}") + # Continue with existing schema if migration fails + def migrate_database(self) -> None: """ Apply any pending database migrations. - + This method serves as an entry point for future schema migrations. Add new migration logic here as the database evolves. """ diff --git a/database/operations.py b/database/operations.py index 095388d..34d58c8 100644 --- a/database/operations.py +++ b/database/operations.py @@ -654,11 +654,12 @@ class PromptDatabase: # Proceed with linking filename = os.path.basename(image_path) file_info = metadata.get('file_info', {}) if metadata else {} - + + # Use INSERT OR IGNORE to skip duplicates (same prompt_id + filename) cursor = conn.execute( """ - INSERT INTO generated_images - (prompt_id, image_path, filename, file_size, width, height, format, + INSERT OR IGNORE INTO generated_images + (prompt_id, image_path, filename, file_size, width, height, format, workflow_data, prompt_metadata, parameters) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, @@ -676,6 +677,11 @@ class PromptDatabase: ) ) conn.commit() + + if cursor.lastrowid == 0: + self.logger.debug(f"Image {filename} already linked to prompt {prompt_id_int}") + return 0 + return cursor.lastrowid except Exception as e: @@ -685,18 +691,18 @@ class PromptDatabase: def get_prompt_images(self, prompt_id: str) -> List[Dict[str, Any]]: """ Get all images associated with a prompt. - + Args: prompt_id: The prompt ID - + Returns: List of image records """ with self.model.get_connection() as conn: cursor = conn.execute( """ - SELECT * FROM generated_images - WHERE prompt_id = ? + SELECT * FROM generated_images + WHERE prompt_id = ? ORDER BY generation_time DESC """, (prompt_id,) diff --git a/prompt_manager.py b/prompt_manager.py index 636dd41..26f5473 100644 --- a/prompt_manager.py +++ b/prompt_manager.py @@ -37,8 +37,8 @@ except ImportError: try: from .database.operations import PromptDatabase from .utils.comfyui_integration import get_comfyui_integration - from .utils.image_monitor import ImageMonitor - from .utils.prompt_tracker import PromptExecutionContext, PromptTracker + from .utils.image_monitor import get_image_monitor + from .utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker except ImportError: # For direct imports when not in a package import os @@ -47,8 +47,8 @@ except ImportError: sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from database.operations import PromptDatabase from utils.comfyui_integration import get_comfyui_integration - from utils.image_monitor import ImageMonitor - from utils.prompt_tracker import PromptExecutionContext, PromptTracker + from utils.image_monitor import get_image_monitor + from utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker class PromptManager(ComfyNodeABC): @@ -65,8 +65,9 @@ class PromptManager(ComfyNodeABC): self.logger.debug("Initializing PromptManager node") self.db = PromptDatabase() - self.prompt_tracker = PromptTracker(self.db) - self.image_monitor = ImageMonitor(self.db, self.prompt_tracker) + # Use singleton getters to ensure only one tracker/monitor exists + self.prompt_tracker = get_prompt_tracker(self.db) + self.image_monitor = get_image_monitor(self.db, self.prompt_tracker) self.comfyui_integration = get_comfyui_integration() # Start image monitoring automatically @@ -132,7 +133,7 @@ class PromptManager(ComfyNodeABC): "A conditioning containing the embedded text used to guide the diffusion model.", "The final combined text string (with prepend/append applied) that was encoded.", ) - FUNCTION = "encode" + FUNCTION = "encode_prompt" CATEGORY = "🫶 ComfyAssets/🧠 Prompts" DESCRIPTION = ( "Encodes a text prompt using a CLIP model into an embedding that can be used to guide " @@ -140,7 +141,7 @@ class PromptManager(ComfyNodeABC): "to a local SQLite database with optional metadata for search and retrieval." ) - def encode( + def encode_prompt( self, clip, text: str, @@ -168,11 +169,18 @@ class PromptManager(ComfyNodeABC): Raises: RuntimeError: If clip input is invalid """ + # Debug: Log text being encoded - this should print EVERY time the node executes + import time + print(f"\n{'='*60}") + print(f"[PromptManager] encode_prompt() CALLED at {time.time()}") + print(f"[PromptManager] text = {repr(text)[:100]}") + print(f"{'='*60}\n") + # Combine prepend, main text, and append text final_text = "" if prepend_text and prepend_text.strip(): final_text += prepend_text.strip() + " " - final_text += text + final_text += text if text else "" if append_text and append_text.strip(): final_text += " " + append_text.strip() @@ -259,7 +267,7 @@ class PromptManager(ComfyNodeABC): }, ) - self.logger.debug("CLIP encoding completed successfully") + self.logger.info(f"CLIP encoding completed, text: {repr(encoding_text)[:80]}") return (conditioning, encoding_text) def _save_prompt_to_database( @@ -484,25 +492,6 @@ class PromptManager(ComfyNodeABC): """ self.cleanup_gallery_system() - @classmethod - def IS_CHANGED(cls, text="", category="", tags="", search_text="", - prepend_text="", append_text="", **kwargs): - """ - ComfyUI method to determine if node needs re-execution. - - This method now properly tracks input changes to avoid unnecessary - re-execution while still ensuring prompts are saved when inputs change. - - Returns: - A hash of the input values that changes when any input changes - """ - # Create a hash of all the text inputs that affect the output - # This ensures the node only re-executes when inputs actually change - import hashlib - - # Combine all text inputs that affect the conditioning output - combined = f"{text}|{category}|{tags}|{prepend_text}|{append_text}" - - # Return a hash that will change when inputs change - # Note: We don't include search_text as it doesn't affect the output conditioning - return hashlib.sha256(combined.encode()).hexdigest() + # NOTE: IS_CHANGED intentionally removed to match CLIPTextEncode behavior + # ComfyUI's default caching (based on input values) should handle cache invalidation + # The previous IS_CHANGED implementation was causing input/cache mismatch issues diff --git a/prompt_manager_text.py b/prompt_manager_text.py index 12ddb25..48d2945 100644 --- a/prompt_manager_text.py +++ b/prompt_manager_text.py @@ -37,8 +37,8 @@ except ImportError: try: from .database.operations import PromptDatabase from .utils.comfyui_integration import get_comfyui_integration - from .utils.image_monitor import ImageMonitor - from .utils.prompt_tracker import PromptExecutionContext, PromptTracker + from .utils.image_monitor import get_image_monitor + from .utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker except ImportError: # For direct imports when not in a package import os @@ -47,8 +47,8 @@ except ImportError: sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from database.operations import PromptDatabase from utils.comfyui_integration import get_comfyui_integration - from utils.image_monitor import ImageMonitor - from utils.prompt_tracker import PromptExecutionContext, PromptTracker + from utils.image_monitor import get_image_monitor + from utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker class PromptManagerText(ComfyNodeABC): @@ -67,8 +67,9 @@ class PromptManagerText(ComfyNodeABC): self.logger.debug("Initializing PromptManagerText node") self.db = PromptDatabase() - self.prompt_tracker = PromptTracker(self.db) - self.image_monitor = ImageMonitor(self.db, self.prompt_tracker) + # Use singleton getters to ensure only one tracker/monitor exists + self.prompt_tracker = get_prompt_tracker(self.db) + self.image_monitor = get_image_monitor(self.db, self.prompt_tracker) self.comfyui_integration = get_comfyui_integration() # Start image monitoring automatically @@ -455,25 +456,6 @@ class PromptManagerText(ComfyNodeABC): """ self.cleanup_gallery_system() - @classmethod - def IS_CHANGED(cls, text="", category="", tags="", search_text="", - prepend_text="", append_text="", **kwargs): - """ - ComfyUI method to determine if node needs re-execution. - - This method now properly tracks input changes to avoid unnecessary - re-execution while still ensuring prompts are saved when inputs change. - - Returns: - A hash of the input values that changes when any input changes - """ - # Create a hash of all the text inputs that affect the output - # This ensures the node only re-executes when inputs actually change - import hashlib - - # Combine all text inputs that affect the output - combined = f"{text}|{category}|{tags}|{prepend_text}|{append_text}" - - # Return a hash that will change when inputs change - # Note: We don't include search_text as it doesn't affect the output - return hashlib.sha256(combined.encode()).hexdigest() + # NOTE: IS_CHANGED intentionally removed to match CLIPTextEncode behavior + # ComfyUI's default caching (based on input values) should handle cache invalidation + # The previous IS_CHANGED implementation was causing input/cache mismatch issues diff --git a/py/api.py b/py/api.py index 71cde29..38b077a 100644 --- a/py/api.py +++ b/py/api.py @@ -302,6 +302,46 @@ class PromptManagerAPI: status=500, ) + # Serve static files from web/lib directory + @routes.get("/prompt_manager/lib/{filepath:.*}") + async def serve_lib_static(request): + """Serve static library files (JS, CSS) from web/lib directory.""" + import os + + # Explicit MIME type mapping + MIME_TYPES = { + ".js": "application/javascript", + ".css": "text/css", + ".json": "application/json", + ".map": "application/json", + } + + filepath = request.match_info.get("filepath", "") + + # Security: prevent directory traversal + if ".." in filepath or filepath.startswith("/"): + return web.Response(text="Forbidden", status=403) + + current_dir = os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + ) + file_path = os.path.join(current_dir, "web", "lib", filepath) + + if not os.path.exists(file_path) or not os.path.isfile(file_path): + return web.Response(text=f"Not Found: {filepath}", status=404) + + # Get extension and content type + ext = os.path.splitext(file_path)[1].lower() + content_type = MIME_TYPES.get(ext, "application/octet-stream") + + with open(file_path, "rb") as f: + content = f.read() + + return web.Response( + body=content, + content_type=content_type, + ) + # Statistics endpoint @routes.get("/prompt_manager/stats") async def get_stats_route(request): @@ -1021,10 +1061,11 @@ class PromptManagerAPI: thumbnails_dir = output_path / "thumbnails" if thumbnails_dir.exists(): thumbnail_ext = '.jpg' if is_video else extension - # Use full relative path to support subdirectories - thumbnail_rel_path = Path("thumbnails") / rel_path.parent / f"{media_path.stem}_thumb{thumbnail_ext}" + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') + thumbnail_rel_path = f"thumbnails/{rel_path_no_ext.as_posix()}_thumb{thumbnail_ext}" thumbnail_abs_path = output_path / thumbnail_rel_path - + if thumbnail_abs_path.exists(): from urllib.parse import quote thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel_path.as_posix(), safe="/")}' @@ -1148,9 +1189,9 @@ class PromptManagerAPI: # Also try to delete associated thumbnail if it exists try: - # Use full relative path to support subdirectories rel_path = file_path.relative_to(output_path) - thumbnail_path = output_path / "thumbnails" / rel_path.parent / f"{file_path.stem}_thumb{file_path.suffix}" + rel_path_no_ext = rel_path.with_suffix('') + thumbnail_path = output_path / "thumbnails" / f"{rel_path_no_ext.as_posix()}_thumb{file_path.suffix}" if thumbnail_path.exists(): os.remove(thumbnail_path) self.logger.debug(f"Deleted associated thumbnail: {thumbnail_path}") @@ -1912,9 +1953,11 @@ class PromptManagerAPI: # For videos, look for thumbnail with .jpg extension # Use full relative path to support subdirectories thumbnail_ext = '.jpg' if is_video else extension - thumbnail_rel_path = Path("thumbnails") / rel_path.parent / f"{media_path.stem}_thumb{thumbnail_ext}" + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') + thumbnail_rel_path = f"thumbnails/{rel_path_no_ext.as_posix()}_thumb{thumbnail_ext}" thumbnail_abs_path = output_path / thumbnail_rel_path - + if thumbnail_abs_path.exists(): from urllib.parse import quote thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel_path.as_posix(), safe="/")}' @@ -2165,11 +2208,12 @@ class PromptManagerAPI: is_video = any(media_file.name.lower().endswith(ext) for ext in video_extensions) # For videos, always save thumbnail as .jpg - # Include parent directory structure to avoid collisions with same-named files + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') if is_video: - thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb.jpg" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg" else: - thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb{rel_path.suffix}" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{rel_path.suffix}" # Skip if thumbnail already exists and is newer than original if (thumbnail_path.exists() and @@ -2382,11 +2426,12 @@ class PromptManagerAPI: rel_path = media_file.relative_to(output_path) # For videos, always save thumbnail as .jpg - # Include parent directory structure to avoid collisions with same-named files + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') if is_video: - thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb.jpg" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg" else: - thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb{rel_path.suffix}" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{rel_path.suffix}" # SAFETY: Ensure thumbnail path is within our thumbnails directory try: @@ -3002,7 +3047,27 @@ class PromptManagerAPI: 'status': 'ok' if output_dirs else 'warning', 'output_dirs': output_dirs } - + + # Check image monitor status + try: + from ..utils.image_monitor import _monitor_instance + if _monitor_instance is not None: + monitor_status = _monitor_instance.get_status() + results['image_monitor'] = { + 'status': 'ok' if monitor_status.get('observer_alive') else 'error', + **monitor_status + } + else: + results['image_monitor'] = { + 'status': 'error', + 'message': 'Image monitor not initialized' + } + except Exception as e: + results['image_monitor'] = { + 'status': 'error', + 'message': f'Failed to get monitor status: {str(e)}' + } + return web.json_response({ 'success': True, 'diagnostics': results @@ -4747,12 +4812,11 @@ class PromptManagerAPI: thumbnail_url = None thumbnails_dir = output_path / "thumbnails" if thumbnails_dir.exists(): - # Use full relative path to support subdirectories - thumbnail_rel = Path("thumbnails") / rel_path.parent / f"{image_path.stem}_thumb{image_path.suffix}" - thumbnail_path = output_path / thumbnail_rel + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}" if thumbnail_path.exists(): - from urllib.parse import quote - thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel.as_posix(), safe="/")}' + thumbnail_url = f'/prompt_manager/images/serve/thumbnails/{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}' images.append({ 'filename': image_path.name, diff --git a/pyproject.toml b/pyproject.toml index 6a1f82d..29f414f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "promptmanager" description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite." -version = "3.0.26" +version = "3.0.27" license = {file = "LICENSE"} dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"] diff --git a/restart_gallery.py b/restart_gallery.py index a4b3a5d..f734263 100644 --- a/restart_gallery.py +++ b/restart_gallery.py @@ -30,16 +30,16 @@ sys.path.insert(0, current_dir) try: # Import and initialize the gallery system from database.operations import PromptDatabase - from utils.prompt_tracker import PromptTracker - from utils.image_monitor import ImageMonitor - + from utils.prompt_tracker import get_prompt_tracker + from utils.image_monitor import get_image_monitor + print("[RESTART] Restarting PromptManager Gallery System...") - - # Initialize components + + # Initialize components using singleton getters db = PromptDatabase() - tracker = PromptTracker(db) - monitor = ImageMonitor(db, tracker) - + tracker = get_prompt_tracker(db) + monitor = get_image_monitor(db, tracker) + print("[SUCCESS] Components initialized successfully") # Test image monitoring directories diff --git a/utils/image_monitor.py b/utils/image_monitor.py index 0f28cc3..4c46241 100644 --- a/utils/image_monitor.py +++ b/utils/image_monitor.py @@ -75,11 +75,11 @@ class ImageGenerationHandler(FileSystemEventHandler): event: FileSystemEvent object containing event details """ if not event.is_directory and self.is_image_file(event.src_path): - self.logger.debug(f"New image detected: {event.src_path}") + self.logger.info(f"New image detected: {event.src_path}") # Small delay to ensure file is fully written threading.Timer( - self.processing_delay, - self.process_new_image, + self.processing_delay, + self.process_new_image, args=[event.src_path] ).start() @@ -108,15 +108,15 @@ class ImageGenerationHandler(FileSystemEventHandler): image_path: Full path to the newly created image file """ try: - self.logger.debug(f"Processing image: {image_path}") - + self.logger.info(f"Processing image: {image_path}") + if not os.path.exists(image_path): self.logger.warning(f"Image file no longer exists: {image_path}") return - + # Get current prompt context first current_prompt = self.prompt_tracker.get_current_prompt() - self.logger.debug(f"Current prompt context: {current_prompt['id'] if current_prompt else 'None'}") + self.logger.info(f"Current prompt context: {current_prompt['id'] if current_prompt else 'None'}") if not current_prompt: self.logger.debug(f"No active prompt context for image: {image_path}") @@ -301,13 +301,13 @@ class ImageMonitor: if os.path.exists(output_dir): self.observer.schedule(self.handler, output_dir, recursive=True) self.monitored_directories.append(output_dir) - self.logger.debug(f"Monitoring directory: {output_dir}") + self.logger.info(f"Monitoring directory (recursive): {output_dir}") else: self.logger.warning(f"Directory does not exist: {output_dir}") - + if self.monitored_directories: self.observer.start() - self.logger.debug(f"Image monitoring started for {len(self.monitored_directories)} directories") + self.logger.info(f"Image monitoring started for {len(self.monitored_directories)} directories") else: self.logger.warning("No valid directories to monitor") @@ -367,15 +367,49 @@ class ImageMonitor: def get_status(self) -> Dict[str, Any]: """Get monitoring status information. - + Returns: Dictionary containing: - running: Boolean indicating if monitoring is active - monitored_directories: List of currently monitored directory paths - handler_active: Boolean indicating if the event handler is active + - observer_alive: Boolean indicating if observer thread is alive """ + observer_alive = False + if self.observer is not None: + try: + observer_alive = self.observer.is_alive() + except Exception: + pass return { 'running': self.observer is not None, 'monitored_directories': self.monitored_directories, - 'handler_active': self.handler is not None - } \ No newline at end of file + 'handler_active': self.handler is not None, + 'observer_alive': observer_alive + } + + +# Singleton instance management +_monitor_instance: Optional[ImageMonitor] = None +_monitor_lock = threading.Lock() + + +def get_image_monitor(db_manager, prompt_tracker) -> ImageMonitor: + """Get or create the singleton ImageMonitor instance. + + This ensures only one ImageMonitor exists across all PromptManager nodes, + preventing duplicate image detection when multiple nodes are used. + + Args: + db_manager: Database manager instance for storing image relationships + prompt_tracker: Prompt tracking instance for getting active prompt context + + Returns: + The singleton ImageMonitor instance + """ + global _monitor_instance + if _monitor_instance is None: + with _monitor_lock: + if _monitor_instance is None: + _monitor_instance = ImageMonitor(db_manager, prompt_tracker) + return _monitor_instance \ No newline at end of file diff --git a/utils/prompt_tracker.py b/utils/prompt_tracker.py index d8dd374..597933c 100644 --- a/utils/prompt_tracker.py +++ b/utils/prompt_tracker.py @@ -408,16 +408,41 @@ class PromptExecutionContext: def __exit__(self, exc_type, exc_val, exc_tb): """Exit the execution context. - + Currently does not clear the prompt context to allow images to be generated after prompt execution completes. The prompt will be cleaned up automatically after the timeout period. - + Args: exc_type: Exception type (if an exception occurred) - exc_val: Exception value (if an exception occurred) + exc_val: Exception value (if an exception occurred) exc_tb: Exception traceback (if an exception occurred) """ # Don't clear immediately - let the timeout handle it # This allows images to be generated after the prompt execution completes - pass \ No newline at end of file + pass + + +# Singleton instance management +_tracker_instance: Optional[PromptTracker] = None +_tracker_lock = threading.Lock() + + +def get_prompt_tracker(db_manager) -> PromptTracker: + """Get or create the singleton PromptTracker instance. + + This ensures only one PromptTracker exists across all PromptManager nodes, + preventing duplicate image linking when multiple nodes are used. + + Args: + db_manager: Database manager instance for prompt operations + + Returns: + The singleton PromptTracker instance + """ + global _tracker_instance + if _tracker_instance is None: + with _tracker_lock: + if _tracker_instance is None: + _tracker_instance = PromptTracker(db_manager) + return _tracker_instance \ No newline at end of file diff --git a/web/admin.html b/web/admin.html index 46c4423..8e1f347 100644 --- a/web/admin.html +++ b/web/admin.html @@ -4,9 +4,9 @@ PromptManager - Admin Dashboard - - - + + + - - + + + +