From 85f93c437fdbddda307c294a666e0b1d7f4fdd02 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:08:23 -0800 Subject: [PATCH 01/14] feat(init.py): add global image monitoring system --- __init__.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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 From 86815913c14fc24d157b0bcebc075851ddcf1c0b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:08:34 -0800 Subject: [PATCH 02/14] feat(models): add UNIQUE constraint for prompt_id, filename --- database/models.py | 98 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) 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. """ From d4f13e40b676948e8e326a53a835e974f12d1a52 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:08:44 -0800 Subject: [PATCH 03/14] fix(database): skip duplicates in image linking --- database/operations.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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,) From 8fa490a82a4b1ae89b18f97b7cad8b62fe11ed68 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:08:56 -0800 Subject: [PATCH 04/14] refactor(prompt_manager): update imports and function names --- prompt_manager.py | 53 +++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 32 deletions(-) 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 From 4dbc553b0412c889446fb859eb552b1a4e0373f0 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:09:15 -0800 Subject: [PATCH 05/14] refactor: Use singleton getters for tracker/monitor --- prompt_manager_text.py | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) 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 From 3fe2ee507568ee0bcf4871e4bbe93898122cc20f Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:09:25 -0800 Subject: [PATCH 06/14] feat(api): serve static files and update thumbnail paths --- py/api.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/py/api.py b/py/api.py index e0e10f7..fe293e8 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,9 +1061,11 @@ class PromptManagerAPI: thumbnails_dir = output_path / "thumbnails" if thumbnails_dir.exists(): thumbnail_ext = '.jpg' if is_video else extension - thumbnail_rel_path = f"thumbnails/{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(): thumbnail_url = f'/prompt_manager/images/serve/{Path(thumbnail_rel_path).as_posix()}' @@ -1146,7 +1188,9 @@ class PromptManagerAPI: # Also try to delete associated thumbnail if it exists try: - thumbnail_path = output_path / "thumbnails" / f"{file_path.stem}_thumb{file_path.suffix}" + rel_path = file_path.relative_to(output_path) + 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}") @@ -1907,9 +1951,11 @@ class PromptManagerAPI: if thumbnails_dir.exists(): # For videos, look for thumbnail with .jpg extension thumbnail_ext = '.jpg' if is_video else extension - thumbnail_rel_path = f"thumbnails/{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(): thumbnail_url = f'/prompt_manager/images/serve/{Path(thumbnail_rel_path).as_posix()}' @@ -2159,10 +2205,12 @@ class PromptManagerAPI: is_video = any(media_file.name.lower().endswith(ext) for ext in video_extensions) # For videos, always save thumbnail as .jpg + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') if is_video: - thumbnail_path = thumbnails_dir / f"{rel_path.stem}_thumb.jpg" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg" else: - thumbnail_path = thumbnails_dir / 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 @@ -2375,10 +2423,12 @@ class PromptManagerAPI: rel_path = media_file.relative_to(output_path) # For videos, always save thumbnail as .jpg + # Preserve subdirectory structure in thumbnail path + rel_path_no_ext = rel_path.with_suffix('') if is_video: - thumbnail_path = thumbnails_dir / f"{rel_path.stem}_thumb.jpg" + thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg" else: - thumbnail_path = thumbnails_dir / 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: @@ -2994,7 +3044,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 @@ -4739,9 +4809,11 @@ class PromptManagerAPI: thumbnail_url = None thumbnails_dir = output_path / "thumbnails" if thumbnails_dir.exists(): - thumbnail_path = thumbnails_dir / f"{image_path.stem}_thumb{image_path.suffix}" + # 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(): - thumbnail_url = f'/prompt_manager/images/serve/thumbnails/{image_path.stem}_thumb{image_path.suffix}' + thumbnail_url = f'/prompt_manager/images/serve/thumbnails/{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}' images.append({ 'filename': image_path.name, From 47d3f4d2795bde9997f7040fd1979873406a03d0 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:09:38 -0800 Subject: [PATCH 07/14] refactor: use singleton getters for initialization --- restart_gallery.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 From c50a7292c75806342a8ef7df5a3b42798a199ccc Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:09:48 -0800 Subject: [PATCH 08/14] refactor(image_monitor): use info log level instead of debug --- utils/image_monitor.py | 60 +++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) 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 From c3aa41d679c8453ce78884e47a0033b9dcd6073f Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:09:58 -0800 Subject: [PATCH 09/14] feat: Add singleton management for PromptTracker --- utils/prompt_tracker.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) 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 From 15e828b7a6d931680ba1fa3e4cb6445039d7ddc8 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:10:06 -0800 Subject: [PATCH 10/14] build(web): update library paths in admin.html --- web/admin.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/admin.html b/web/admin.html index 154ee95..771ac91 100644 --- a/web/admin.html +++ b/web/admin.html @@ -4,9 +4,9 @@ PromptManager - Admin Dashboard - - - + + + - - + + + +