diff --git a/__init__.py b/__init__.py index ed030a8..44dad7f 100644 --- a/__init__.py +++ b/__init__.py @@ -3,15 +3,30 @@ PromptManager: A ComfyUI custom node that extends the standard text encoder with persistent prompt storage and advanced search capabilities using SQLite. """ -print("[PromptManager] Starting to load PromptManager custom node...") +# Import logging system +try: + from .utils.logging_config import get_logger + init_logger = get_logger('prompt_manager.init') + init_logger.info("Starting to load PromptManager custom node...") +except ImportError: + # Fallback to print if logging isn't available yet + init_logger = None + print("[PromptManager] Starting to load PromptManager custom node...") + +def log_message(message, level='info'): + """Helper to log messages with fallback to print.""" + if init_logger: + getattr(init_logger, level)(message.replace("[PromptManager] ", "")) + else: + print(f"[PromptManager] {message}") try: from .prompt_manager import PromptManager - print("[PromptManager] Successfully imported PromptManager class") + log_message("Successfully imported PromptManager class") except Exception as e: - print(f"[PromptManager] ERROR: Failed to import PromptManager class: {e}") + log_message(f"ERROR: Failed to import PromptManager class: {e}", 'error') import traceback - print(traceback.format_exc()) + log_message(f"Traceback: {traceback.format_exc()}", 'error') raise NODE_CLASS_MAPPINGS = { @@ -22,8 +37,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "PromptManager": "Prompt Manager", } -print(f"[PromptManager] NODE_CLASS_MAPPINGS: {NODE_CLASS_MAPPINGS}") -print(f"[PromptManager] NODE_DISPLAY_NAME_MAPPINGS: {NODE_DISPLAY_NAME_MAPPINGS}") +log_message(f"NODE_CLASS_MAPPINGS: {NODE_CLASS_MAPPINGS}") +log_message(f"NODE_DISPLAY_NAME_MAPPINGS: {NODE_DISPLAY_NAME_MAPPINGS}") # Define path to web directory for UI components WEB_DIRECTORY = "web" @@ -43,15 +58,15 @@ try: api = PromptManagerAPI() api.add_routes(routes) - print("[PromptManager] API routes registered successfully") - print(f"[PromptManager] Server instance: {config.server_instance}") - print(f"[PromptManager] Routes object: {routes}") + log_message("API routes registered successfully") + log_message(f"Server instance: {config.server_instance}") + log_message(f"Routes object: {routes}") except Exception as e: - print(f"[PromptManager] Failed to register API routes: {e}") + log_message(f"Failed to register API routes: {e}", 'error') import traceback - print(traceback.format_exc()) + log_message(f"Traceback: {traceback.format_exc()}", 'error') __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] -print("[PromptManager] โ PromptManager custom node loaded successfully!") \ No newline at end of file +log_message("โ PromptManager custom node loaded successfully!") \ No newline at end of file diff --git a/database/models.py b/database/models.py index f2d53d3..0e99283 100644 --- a/database/models.py +++ b/database/models.py @@ -6,6 +6,15 @@ import sqlite3 import os from typing import Optional +# Import logging system +try: + from ..utils.logging_config import get_logger +except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger + class PromptModel: """Database model for prompt storage and schema management.""" @@ -17,6 +26,8 @@ class PromptModel: Args: db_path: Path to the SQLite database file """ + self.logger = get_logger('prompt_manager.database.models') + self.logger.info(f"Initializing database model with path: {db_path}") self.db_path = db_path self._ensure_database_exists() @@ -29,7 +40,7 @@ class PromptModel: self._create_indexes(conn) conn.commit() except Exception as e: - print(f"Error creating database: {e}") + self.logger.error(f"Error creating database: {e}") raise def _create_tables(self, conn: sqlite3.Connection) -> None: @@ -109,7 +120,7 @@ class PromptModel: columns = [column[1] for column in cursor.fetchall()] if 'workflow_name' in columns: - print("[KikoTextEncode] Migrating database: removing workflow_name column") + self.logger.info("Migrating database: removing workflow_name column") # Create new table without workflow_name conn.execute(""" @@ -137,10 +148,10 @@ class PromptModel: conn.execute("DROP TABLE prompts") conn.execute("ALTER TABLE prompts_new RENAME TO prompts") - print("[KikoTextEncode] Database migration completed") + self.logger.info("Database migration completed") except Exception as e: - print(f"[KikoTextEncode] Migration error: {e}") + self.logger.error(f"Migration error: {e}") # If migration fails, the table creation will handle it def _migrate_foreign_key_types(self, conn: sqlite3.Connection) -> None: @@ -151,7 +162,7 @@ class PromptModel: columns = {column[1]: column[2] for column in cursor.fetchall()} if 'prompt_id' in columns and columns['prompt_id'] == 'TEXT': - print("[KikoTextEncode] Migrating foreign key types: prompt_id TEXT -> INTEGER") + self.logger.info("Migrating foreign key types: prompt_id TEXT -> INTEGER") # Create new table with correct types conn.execute(""" @@ -188,10 +199,10 @@ class PromptModel: conn.execute("DROP TABLE generated_images") conn.execute("ALTER TABLE generated_images_new RENAME TO generated_images") - print("[KikoTextEncode] Foreign key migration completed") + self.logger.info("Foreign key migration completed") except Exception as e: - print(f"[KikoTextEncode] Foreign key migration error: {e}") + self.logger.error(f"Foreign key migration error: {e}") # If migration fails, continue with existing schema def migrate_database(self) -> None: @@ -206,7 +217,7 @@ class PromptModel: conn.execute("VACUUM") conn.commit() except Exception as e: - print(f"Error vacuuming database: {e}") + self.logger.error(f"Error vacuuming database: {e}") def get_database_info(self) -> dict: """ @@ -241,7 +252,7 @@ class PromptModel: 'database_path': os.path.abspath(self.db_path) } except Exception as e: - print(f"Error getting database info: {e}") + self.logger.error(f"Error getting database info: {e}") return {} def backup_database(self, backup_path: str) -> bool: @@ -259,5 +270,5 @@ class PromptModel: shutil.copy2(self.db_path, backup_path) return True except Exception as e: - print(f"Error creating database backup: {e}") + self.logger.error(f"Error creating database backup: {e}") return False \ No newline at end of file diff --git a/database/operations.py b/database/operations.py index 017e63c..6115753 100644 --- a/database/operations.py +++ b/database/operations.py @@ -10,6 +10,15 @@ from typing import Optional, List, Dict, Any, Union from .models import PromptModel +# Import logging system +try: + from ..utils.logging_config import get_logger +except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger + class PromptDatabase: """Database operations class for managing prompts.""" @@ -21,7 +30,10 @@ class PromptDatabase: Args: db_path: Path to the SQLite database file """ + self.logger = get_logger('prompt_manager.database') + self.logger.info(f"Initializing database operations with path: {db_path}") self.model = PromptModel(db_path) + self.logger.debug("Database operations initialized successfully") def save_prompt( self, @@ -58,6 +70,8 @@ class PromptDatabase: tags_json = json.dumps(tags) if tags else None + self.logger.debug(f"Saving prompt: text_length={len(text)}, category={category}, tags={tags}, rating={rating}") + with self.model.get_connection() as conn: cursor = conn.execute( """ @@ -77,7 +91,9 @@ class PromptDatabase: ) ) conn.commit() - return cursor.lastrowid + prompt_id = cursor.lastrowid + self.logger.info(f"Successfully saved prompt with ID: {prompt_id}") + return prompt_id def get_prompt_by_id(self, prompt_id: int) -> Optional[Dict[str, Any]]: """ @@ -410,7 +426,7 @@ class PromptDatabase: return True except Exception as e: - print(f"Error exporting prompts: {e}") + self.logger.error(f"Error exporting prompts: {e}") return False def cleanup_duplicates(self) -> int: @@ -420,6 +436,7 @@ class PromptDatabase: Returns: int: Number of duplicates removed """ + self.logger.info("Starting duplicate cleanup process") try: with self.model.get_connection() as conn: # Find duplicates by text content (case-insensitive) @@ -432,6 +449,7 @@ class PromptDatabase: """) duplicates = cursor.fetchall() + self.logger.debug(f"Found {len(duplicates)} groups of duplicate prompts") total_removed = 0 for duplicate in duplicates: @@ -449,12 +467,14 @@ class PromptDatabase: conn.commit() if total_removed > 0: - print(f"[KikoTextEncode] Removed {total_removed} duplicate prompts") + self.logger.info(f"Removed {total_removed} duplicate prompts") + else: + self.logger.info("No duplicate prompts found") return total_removed except Exception as e: - print(f"Error cleaning up duplicates: {e}") + self.logger.error(f"Error cleaning up duplicates: {e}") return 0 # Gallery-related methods diff --git a/prompt_manager.py b/prompt_manager.py index b6a5b83..6fb19a1 100644 --- a/prompt_manager.py +++ b/prompt_manager.py @@ -10,6 +10,14 @@ import webbrowser import os from typing import Optional, Dict, Any, Tuple, List +# Import logging system +try: + from .utils.logging_config import get_logger +except ImportError: + import sys + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from utils.logging_config import get_logger + try: from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict except ImportError: @@ -48,12 +56,16 @@ class PromptManager(ComfyNodeABC): """ def __init__(self): + self.logger = get_logger('prompt_manager.node') + self.logger.info("Initializing PromptManager node") + self.db = PromptDatabase() self.prompt_tracker = PromptTracker(self.db) self.image_monitor = ImageMonitor(self.db, self.prompt_tracker) # Start image monitoring automatically self._start_gallery_system() + self.logger.info("PromptManager node initialization completed") @classmethod def INPUT_TYPES(cls) -> InputTypeDict: @@ -125,15 +137,18 @@ class PromptManager(ComfyNodeABC): # Validate CLIP model if clip is None: - raise RuntimeError( + error_msg = ( "ERROR: clip input is invalid: None\n\n" "If the clip is from a checkpoint loader node your checkpoint does not " "contain a valid clip or text encoder model." ) + self.logger.error("CLIP validation failed: clip input is None") + raise RuntimeError(error_msg) # Save prompt to database and set execution context for gallery tracking prompt_id = None if text and text.strip(): + self.logger.debug(f"Processing prompt text: {text[:100]}...") try: prompt_id = self._save_prompt_to_database( text=text.strip(), # Always strip whitespace @@ -151,16 +166,19 @@ class PromptManager(ComfyNodeABC): 'prompt_id': prompt_id } ) - print(f"[PromptManager] Set execution context: {execution_id}") + self.logger.info(f"Set execution context: {execution_id} for prompt ID: {prompt_id}") except Exception as e: # Log error but don't fail the encoding - print(f"Warning: Failed to save prompt to database: {e}") + self.logger.warning(f"Failed to save prompt to database: {e}") + # Already logged above, no need for additional print # Perform standard CLIP text encoding + self.logger.debug("Performing CLIP text encoding") tokens = clip.tokenize(text) conditioning = clip.encode_from_tokens_scheduled(tokens) + self.logger.debug("CLIP encoding completed successfully") return (conditioning,) def _save_prompt_to_database( @@ -183,10 +201,12 @@ class PromptManager(ComfyNodeABC): try: # Generate hash for duplicate detection prompt_hash = self._generate_hash(text) + self.logger.debug(f"Generated hash for prompt: {prompt_hash[:16]}...") # Check if prompt already exists existing = self.db.get_prompt_by_hash(prompt_hash) if existing: + self.logger.info(f"Found existing prompt with ID {existing['id']}, updating metadata") # Update metadata if this is a duplicate with new info if any([category, tags]): self.db.update_prompt_metadata( @@ -194,9 +214,11 @@ class PromptManager(ComfyNodeABC): category=category, tags=tags ) + self.logger.debug("Updated metadata for existing prompt") return existing['id'] # Save new prompt + self.logger.debug(f"Saving new prompt with category: {category}, tags: {tags}") prompt_id = self.db.save_prompt( text=text, category=category, @@ -204,10 +226,16 @@ class PromptManager(ComfyNodeABC): prompt_hash=prompt_hash ) + if prompt_id: + self.logger.info(f"Successfully saved new prompt with ID: {prompt_id}") + else: + self.logger.warning("Failed to save prompt - no ID returned") + return prompt_id except Exception as e: - print(f"Error saving prompt to database: {e}") + self.logger.error(f"Error saving prompt to database: {e}") + # Already logged above, no need for additional print return None def _generate_hash(self, text: str) -> str: @@ -241,7 +269,7 @@ class PromptManager(ComfyNodeABC): return results except Exception as e: - print(f"Error searching prompts: {e}") + self.logger.error(f"Error searching prompts: {e}") return [] def _open_web_interface(self): @@ -256,17 +284,14 @@ class PromptManager(ComfyNodeABC): index_path = os.path.join(web_dir, "index.html") if os.path.exists(index_path): webbrowser.open(f"file://{index_path}") - print("\n=== Web interface opened in browser ===") + self.logger.info("Web interface opened in browser") else: - print("\n=== Web interface directory found but no index.html ===") - print(f"Please check {web_dir} for setup instructions") + self.logger.warning(f"Web interface directory found but no index.html. Please check {web_dir} for setup instructions") else: - print("\n=== Web interface not yet implemented ===") - print("This feature will open a web-based prompt management interface") - print("when the web_interface directory is created.") + self.logger.info("Web interface not yet implemented. This feature will open a web-based prompt management interface when the web_interface directory is created.") except Exception as e: - print(f"Error opening web interface: {e}") + self.logger.error(f"Error opening web interface: {e}") def search_prompts_api(self, search_text: str = "") -> List[Dict[str, Any]]: """API method for JavaScript UI to search prompts.""" @@ -277,22 +302,22 @@ class PromptManager(ComfyNodeABC): try: return self.db.get_recent_prompts(limit=limit) except Exception as e: - print(f"Error getting recent prompts: {e}") + self.logger.error(f"Error getting recent prompts: {e}") return [] def _start_gallery_system(self): """Initialize and start the gallery monitoring system.""" try: - print("[PromptManager] Starting gallery system...") + self.logger.info("Starting gallery system...") # Start image monitoring self.image_monitor.start_monitoring() - print("[PromptManager] Gallery system started successfully") + self.logger.info("Gallery system started successfully") except Exception as e: - print(f"[PromptManager] Failed to start gallery system: {e}") - print("[PromptManager] Gallery features will be disabled") + self.logger.error(f"Failed to start gallery system: {e}") + self.logger.warning("Gallery features will be disabled") def get_gallery_status(self) -> Dict[str, Any]: """Get status of the gallery system.""" @@ -306,9 +331,9 @@ class PromptManager(ComfyNodeABC): try: if hasattr(self, 'image_monitor'): self.image_monitor.stop_monitoring() - print("[PromptManager] Gallery system cleaned up") + self.logger.info("Gallery system cleaned up") except Exception as e: - print(f"[PromptManager] Error cleaning up gallery system: {e}") + self.logger.error(f"Error cleaning up gallery system: {e}") def __del__(self): """Cleanup when object is destroyed.""" diff --git a/py/api.py b/py/api.py index 12c6fef..417050d 100644 --- a/py/api.py +++ b/py/api.py @@ -13,6 +13,7 @@ from PIL import Image # Import database operations try: from ..database.operations import PromptDatabase + from ..utils.logging_config import get_logger except ImportError: # Fallback for when module isn't imported as package import os @@ -20,23 +21,27 @@ except ImportError: sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from database.operations import PromptDatabase + from utils.logging_config import get_logger class PromptManagerAPI: """API class for PromptManager database operations.""" def __init__(self): + self.logger = get_logger('prompt_manager.api') + self.logger.info("Initializing PromptManager API") + self.db = PromptDatabase() # Run cleanup on initialization to remove any existing duplicates try: removed = self.db.cleanup_duplicates() if removed > 0: - print( - f"[PromptManager] Startup cleanup: removed {removed} duplicate prompts" - ) + self.logger.info(f"Startup cleanup: removed {removed} duplicate prompts") except Exception as e: - print(f"[PromptManager] Startup cleanup failed: {e}") + self.logger.error(f"Startup cleanup failed: {e}") + + self.logger.info("PromptManager API initialization completed") def add_routes(self, routes): """Add API routes to ComfyUI server using decorator pattern.""" @@ -250,7 +255,36 @@ class PromptManagerAPI: async def scan_images_route(request): return await self.scan_images(request) - print("[PromptManager] All routes registered with decorator pattern") + # Logging endpoints + @routes.get("/prompt_manager/logs") + async def get_logs_route(request): + return await self.get_logs(request) + + @routes.get("/prompt_manager/logs/files") + async def get_log_files_route(request): + return await self.get_log_files(request) + + @routes.get("/prompt_manager/logs/download/{filename}") + async def download_log_route(request): + return await self.download_log_file(request) + + @routes.post("/prompt_manager/logs/truncate") + async def truncate_logs_route(request): + return await self.truncate_logs(request) + + @routes.get("/prompt_manager/logs/config") + async def get_log_config_route(request): + return await self.get_log_config(request) + + @routes.post("/prompt_manager/logs/config") + async def update_log_config_route(request): + return await self.update_log_config(request) + + @routes.get("/prompt_manager/logs/stats") + async def get_log_stats_route(request): + return await self.get_log_stats(request) + + self.logger.info("All routes registered with decorator pattern") async def search_prompts(self, request): """ @@ -290,8 +324,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Search error: {e}") - print(traceback.format_exc()) + self.logger.error(f"Search error: {e}", exc_info=True) return web.json_response( {"success": False, "error": f"Search failed: {str(e)}", "results": []}, status=500, @@ -312,8 +345,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Recent prompts error: {e}") - print(traceback.format_exc()) + self.logger.error(f"Recent prompts error: {e}", exc_info=True) return web.json_response( { "success": False, @@ -334,7 +366,7 @@ class PromptManagerAPI: return web.json_response({"success": True, "categories": categories}) except Exception as e: - print(f"[PromptManager API] Categories error: {e}") + self.logger.error(f"Categories error: {e}") return web.json_response( { "success": False, @@ -355,7 +387,7 @@ class PromptManagerAPI: return web.json_response({"success": True, "tags": tags}) except Exception as e: - print(f"[PromptManager API] Tags error: {e}") + self.logger.error(f"Tags error: {e}") return web.json_response( { "success": False, @@ -430,8 +462,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Save error: {e}") - print(traceback.format_exc()) + self.logger.error(f"Save error: {e}", exc_info=True) return web.json_response( {"success": False, "error": f"Failed to save prompt: {str(e)}"}, status=500, @@ -465,7 +496,7 @@ class PromptManagerAPI: {"success": False, "error": "Invalid prompt ID"}, status=400 ) except Exception as e: - print(f"[PromptManager API] Delete error: {e}") + self.logger.error(f"Delete error: {e}") return web.json_response( {"success": False, "error": f"Failed to delete prompt: {str(e)}"}, status=500, @@ -488,7 +519,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Cleanup error: {e}") + self.logger.error(f"Cleanup error: {e}") return web.json_response( {"success": False, "error": f"Failed to cleanup duplicates: {str(e)}"}, status=500, @@ -539,7 +570,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Stats error: {e}") + self.logger.error(f"Stats error: {e}") return web.json_response( {"success": False, "error": f"Failed to get statistics: {str(e)}"}, status=500, @@ -611,7 +642,7 @@ class PromptManagerAPI: {"success": False, "error": "Invalid prompt ID"}, status=400 ) except Exception as e: - print(f"[PromptManager API] Update prompt error: {e}") + self.logger.error(f"Update prompt error: {e}") return web.json_response( {"success": False, "error": f"Failed to update prompt: {str(e)}"}, status=500, @@ -655,7 +686,7 @@ class PromptManagerAPI: {"success": False, "error": "Invalid prompt ID"}, status=400 ) except Exception as e: - print(f"[PromptManager API] Update rating error: {e}") + self.logger.error(f"Update rating error: {e}") return web.json_response( {"success": False, "error": f"Failed to update rating: {str(e)}"}, status=500, @@ -710,7 +741,7 @@ class PromptManagerAPI: {"success": False, "error": "Invalid prompt ID"}, status=400 ) except Exception as e: - print(f"[PromptManager API] Add tag error: {e}") + self.logger.error(f"Add tag error: {e}") return web.json_response( {"success": False, "error": f"Failed to add tag: {str(e)}"}, status=500 ) @@ -759,7 +790,7 @@ class PromptManagerAPI: {"success": False, "error": "Invalid prompt ID"}, status=400 ) except Exception as e: - print(f"[PromptManager API] Remove tag error: {e}") + self.logger.error(f"Remove tag error: {e}") return web.json_response( {"success": False, "error": f"Failed to remove tag: {str(e)}"}, status=500, @@ -798,7 +829,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Bulk delete error: {e}") + self.logger.error(f"Bulk delete error: {e}") return web.json_response( {"success": False, "error": f"Failed to delete prompts: {str(e)}"}, status=500, @@ -865,7 +896,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Bulk add tags error: {e}") + self.logger.error(f"Bulk add tags error: {e}") return web.json_response( {"success": False, "error": f"Failed to add tags: {str(e)}"}, status=500 ) @@ -906,7 +937,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Bulk set category error: {e}") + self.logger.error(f"Bulk set category error: {e}") return web.json_response( {"success": False, "error": f"Failed to set category: {str(e)}"}, status=500, @@ -937,7 +968,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Export error: {e}") + self.logger.error(f"Export error: {e}") return web.json_response( {"success": False, "error": f"Failed to export prompts: {str(e)}"}, status=500, @@ -987,7 +1018,7 @@ class PromptManagerAPI: return web.json_response(cleaned_data) except Exception as json_error: - print(f"[PromptManager API] JSON cleaning error: {json_error}") + self.logger.error(f"JSON cleaning error: {json_error}") # Fallback to original response return web.json_response({ 'success': True, @@ -995,7 +1026,7 @@ class PromptManagerAPI: }) except Exception as e: - print(f"[PromptManager API] Get prompt images error: {e}") + self.logger.error(f"Get prompt images error: {e}") return web.json_response({ 'success': False, 'error': str(e) @@ -1012,7 +1043,7 @@ class PromptManagerAPI: 'images': images }) except Exception as e: - print(f"[PromptManager API] Get recent images error: {e}") + self.logger.error(f"Get recent images error: {e}") return web.json_response({ 'success': False, 'error': str(e) @@ -1036,7 +1067,7 @@ class PromptManagerAPI: 'query': query }) except Exception as e: - print(f"[PromptManager API] Search images error: {e}") + self.logger.error(f"Search images error: {e}") return web.json_response({ 'success': False, 'error': str(e) @@ -1083,7 +1114,7 @@ class PromptManagerAPI: except ValueError: return web.json_response({'error': 'Invalid image ID'}, status=400) except Exception as e: - print(f"[PromptManager API] Serve image error: {e}") + self.logger.error(f"Serve image error: {e}") return web.json_response({'error': str(e)}, status=500) async def link_image_to_prompt(self, request): @@ -1118,7 +1149,7 @@ class PromptManagerAPI: }) except Exception as e: - print(f"[PromptManager API] Link image error: {e}") + self.logger.error(f"Link image error: {e}") return web.json_response({ 'success': False, 'error': str(e) @@ -1144,7 +1175,7 @@ class PromptManagerAPI: except ValueError: return web.json_response({'error': 'Invalid image ID'}, status=400) except Exception as e: - print(f"[PromptManager API] Delete image error: {e}") + self.logger.error(f"Delete image error: {e}") return web.json_response({ 'success': False, 'error': str(e) @@ -1241,9 +1272,7 @@ class PromptManagerAPI: }) except Exception as e: - print(f"[PromptManager API] Diagnostics error: {e}") - import traceback - traceback.print_exc() + self.logger.error(f"Diagnostics error: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': str(e) @@ -1298,9 +1327,7 @@ class PromptManagerAPI: }) except Exception as e: - print(f"[PromptManager API] Test link error: {e}") - import traceback - traceback.print_exc() + self.logger.error(f"Test link error: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': str(e) @@ -1456,9 +1483,7 @@ class PromptManagerAPI: }) except Exception as e: - print(f"[PromptManager API] Maintenance error: {e}") - import traceback - traceback.print_exc() + self.logger.error(f"Maintenance error: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': f'Maintenance failed: {str(e)}' @@ -1512,9 +1537,7 @@ class PromptManagerAPI: ) except Exception as e: - print(f"[PromptManager API] Backup error: {e}") - import traceback - traceback.print_exc() + self.logger.error(f"Backup error: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': f'Failed to backup database: {str(e)}' @@ -1586,7 +1609,7 @@ class PromptManagerAPI: # Create backup of current database if it exists if os.path.exists(db_path): shutil.copy2(db_path, backup_path) - print(f"[PromptManager] Current database backed up to: {backup_path}") + self.logger.info(f"Current database backed up to: {backup_path}") # Replace current database with uploaded one shutil.copy2(temp_path, db_path) @@ -1617,9 +1640,7 @@ class PromptManagerAPI: os.unlink(temp_path) except Exception as e: - print(f"[PromptManager API] Restore error: {e}") - import traceback - traceback.print_exc() + self.logger.error(f"Restore error: {e}", exc_info=True) return web.json_response({ 'success': False, 'error': f'Failed to restore database: {str(e)}' @@ -1637,9 +1658,17 @@ class PromptManagerAPI: async def stream_response(): try: + self.logger.info("Starting image scan operation") + + # Clear any active prompt tracking timers to avoid conflicts + # Note: For now we'll skip this since we don't have easy access to the tracker instance + # This is mainly important if scans are run during active generation, which is rare + self.logger.info("Starting scan (timer clearing not implemented yet)") + # Find ComfyUI output directory output_dir = self._find_comfyui_output_dir() if not output_dir: + self.logger.error("ComfyUI output directory not found") yield f"data: {json.dumps({'type': 'error', 'message': 'ComfyUI output directory not found'})}\n\n" return @@ -1658,6 +1687,7 @@ class PromptManagerAPI: processed_count = 0 found_count = 0 added_count = 0 + linked_count = 0 # Count of images linked to existing prompts for i, png_file in enumerate(png_files): try: @@ -1666,11 +1696,11 @@ class PromptManagerAPI: processed_count += 1 if metadata: - print(f"[PromptManager] Found metadata in {os.path.basename(png_file)}: {list(metadata.keys())}") + self.logger.debug(f"Found metadata in {os.path.basename(png_file)}: {list(metadata.keys())}") # Parse ComfyUI prompt data parsed_data = self._parse_comfyui_prompt(metadata) - print(f"[PromptManager] Parsed data keys: {list(parsed_data.keys())}, has prompt: {bool(parsed_data.get('prompt'))}, has parameters: {bool(parsed_data.get('parameters'))}") + self.logger.debug(f"Parsed data keys: {list(parsed_data.keys())}, has prompt: {bool(parsed_data.get('prompt'))}, has parameters: {bool(parsed_data.get('parameters'))}") # Check if we found any meaningful prompt data if parsed_data.get('prompt') or parsed_data.get('parameters'): @@ -1681,13 +1711,13 @@ class PromptManagerAPI: # Debug: print what we found and its type if prompt_text: - print(f"[PromptManager] Found prompt in {os.path.basename(png_file)} (type: {type(prompt_text)}): {str(prompt_text)[:100]}...") + self.logger.debug(f"Found prompt in {os.path.basename(png_file)} (type: {type(prompt_text)}): {str(prompt_text)[:100]}...") else: - print(f"[PromptManager] No readable prompt found in {os.path.basename(png_file)}, parsed_data keys: {list(parsed_data.keys())}") + self.logger.debug(f"No readable prompt found in {os.path.basename(png_file)}, parsed_data keys: {list(parsed_data.keys())}") # Ensure prompt_text is a string if prompt_text and not isinstance(prompt_text, str): - print(f"[PromptManager] Converting prompt_text from {type(prompt_text)} to string") + self.logger.debug(f"Converting prompt_text from {type(prompt_text)} to string") prompt_text = str(prompt_text) if prompt_text and prompt_text.strip(): @@ -1703,17 +1733,22 @@ class PromptManagerAPI: from utils.hashing import generate_prompt_hash prompt_hash = generate_prompt_hash(prompt_text.strip()) + self.logger.debug(f"Generated hash for prompt: {prompt_hash[:16]}...") # Check if prompt already exists existing = self.db.get_prompt_by_hash(prompt_hash) if existing: + self.logger.debug(f"Found existing prompt ID {existing['id']} for image {os.path.basename(png_file)}") # Link image to existing prompt try: self.db.link_image_to_prompt(existing['id'], str(png_file)) + linked_count += 1 + self.logger.debug(f"Linked image {os.path.basename(png_file)} to existing prompt {existing['id']}") except Exception as e: - print(f"[PromptManager] Failed to link image {png_file}: {e}") + self.logger.error(f"Failed to link image {png_file} to existing prompt: {e}") else: # Save new prompt + self.logger.debug(f"Saving new prompt from {os.path.basename(png_file)}") prompt_id = self.db.save_prompt( text=prompt_text.strip(), category='scanned', @@ -1724,15 +1759,19 @@ class PromptManagerAPI: if prompt_id: added_count += 1 + self.logger.info(f"Successfully saved new prompt with ID {prompt_id} from {os.path.basename(png_file)}") # Link image to new prompt try: self.db.link_image_to_prompt(prompt_id, str(png_file)) + self.logger.debug(f"Linked image {os.path.basename(png_file)} to new prompt {prompt_id}") except Exception as e: - print(f"[PromptManager] Failed to link image {png_file}: {e}") + self.logger.error(f"Failed to link image {png_file} to new prompt: {e}") + else: + self.logger.error(f"Failed to save prompt from {os.path.basename(png_file)} - no ID returned") except Exception as e: - print(f"[PromptManager] Failed to save prompt from {png_file}: {e}") + self.logger.error(f"Failed to save prompt from {png_file}: {e}") # Update progress every 10 files or so if i % 10 == 0 or i == total_files - 1: @@ -1745,16 +1784,17 @@ class PromptManagerAPI: await asyncio.sleep(0.01) except Exception as e: - print(f"[PromptManager] Error processing {png_file}: {e}") + self.logger.error(f"Error processing {png_file}: {e}") continue # Send completion message - yield f"data: {json.dumps({'type': 'complete', 'processed': processed_count, 'found': found_count, 'added': added_count})}\n\n" + self.logger.info(f"Scan completed: processed={processed_count}, found={found_count}, new_prompts_added={added_count}, images_linked_to_existing={linked_count}") + yield f"data: {json.dumps({'type': 'complete', 'processed': processed_count, 'found': found_count, 'added': added_count, 'linked': linked_count})}\n\n" except Exception as e: - print(f"[PromptManager] Scan error: {e}") + self.logger.error(f"Scan error: {e}") import traceback - traceback.print_exc() + self.logger.error(f"Scan error traceback: {traceback.format_exc()}") yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n" # Return streaming response @@ -1791,10 +1831,10 @@ class PromptManagerAPI: for path in possible_paths: abs_path = os.path.abspath(path) if os.path.exists(abs_path) and os.path.isdir(abs_path): - print(f"[PromptManager] Found ComfyUI output directory: {abs_path}") + self.logger.info(f"Found ComfyUI output directory: {abs_path}") return abs_path - print("[PromptManager] ComfyUI output directory not found") + self.logger.warning("ComfyUI output directory not found") return None def _extract_comfyui_metadata(self, image_path): @@ -1807,7 +1847,7 @@ class PromptManagerAPI: metadata[key] = value return metadata except Exception as e: - print(f"[PromptManager] Error reading {image_path}: {e}") + self.logger.error(f"Error reading {image_path}: {e}") return {} def _parse_comfyui_prompt(self, metadata): @@ -1886,3 +1926,267 @@ class PromptManagerAPI: return safe_to_string(parsed_data['parameters']['positive']) return None + + # Logging API endpoints + async def get_logs(self, request): + """ + Get recent log entries. + GET /prompt_manager/logs?limit=100&level=INFO + """ + try: + # Import logger here to avoid circular imports + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + + # Get query parameters + limit = int(request.query.get('limit', 100)) + level = request.query.get('level', None) + + # Validate limit + if limit > 1000: + limit = 1000 + elif limit < 1: + limit = 1 + + # Get logs from memory buffer + logs = logger_manager.get_recent_logs(limit=limit, level=level) + + return web.json_response({ + 'success': True, + 'logs': logs, + 'count': len(logs), + 'level_filter': level, + 'limit': limit + }) + + except Exception as e: + self.logger.error(f"Get logs error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e), + 'logs': [] + }, status=500) + + async def get_log_files(self, request): + """ + Get information about available log files. + GET /prompt_manager/logs/files + """ + try: + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + log_files = logger_manager.get_log_files() + + return web.json_response({ + 'success': True, + 'files': log_files, + 'count': len(log_files) + }) + + except Exception as e: + self.logger.error(f"Get log files error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e), + 'files': [] + }, status=500) + + async def download_log_file(self, request): + """ + Download a specific log file. + GET /prompt_manager/logs/download/{filename} + """ + try: + filename = request.match_info['filename'] + + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + + # Validate filename for security + if not filename or '..' in filename or '/' in filename or '\\' in filename: + return web.json_response({ + 'success': False, + 'error': 'Invalid filename' + }, status=400) + + # Get the file content + log_file_path = logger_manager.log_dir / filename + + if not log_file_path.exists(): + return web.json_response({ + 'success': False, + 'error': 'Log file not found' + }, status=404) + + # Read file content + with open(log_file_path, 'rb') as f: + file_content = f.read() + + return web.Response( + body=file_content, + content_type='text/plain', + headers={ + 'Content-Disposition': f'attachment; filename="{filename}"', + 'Content-Length': str(len(file_content)) + } + ) + + except Exception as e: + self.logger.error(f"Download log file error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + async def truncate_logs(self, request): + """ + Truncate all log files. + POST /prompt_manager/logs/truncate + """ + try: + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + results = logger_manager.truncate_logs() + + return web.json_response({ + 'success': True, + 'message': f"Truncated {len(results['truncated'])} log files", + 'results': results + }) + + except Exception as e: + self.logger.error(f"Truncate logs error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + async def get_log_config(self, request): + """ + Get current logging configuration. + GET /prompt_manager/logs/config + """ + try: + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + config = logger_manager.get_config() + + return web.json_response({ + 'success': True, + 'config': config + }) + + except Exception as e: + self.logger.error(f"Get log config error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + async def update_log_config(self, request): + """ + Update logging configuration. + POST /prompt_manager/logs/config + Body: {"level": "DEBUG", "console_logging": true, ...} + """ + try: + data = await request.json() + + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + + # Validate level if provided + if 'level' in data: + valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + if data['level'].upper() not in valid_levels: + return web.json_response({ + 'success': False, + 'error': f'Invalid log level. Must be one of: {valid_levels}' + }, status=400) + data['level'] = data['level'].upper() + + logger_manager.update_config(data) + + return web.json_response({ + 'success': True, + 'message': 'Logging configuration updated', + 'config': logger_manager.get_config() + }) + + except Exception as e: + self.logger.error(f"Update log config error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + async def get_log_stats(self, request): + """ + Get logging statistics. + GET /prompt_manager/logs/stats + """ + try: + try: + from ..utils.logging_config import get_logger_manager + except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger_manager + + logger_manager = get_logger_manager() + stats = logger_manager.get_log_stats() + + return web.json_response({ + 'success': True, + 'stats': stats + }) + + except Exception as e: + self.logger.error(f"Get log stats error: {e}") + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/py/config.py b/py/config.py index fc291b3..82ce676 100644 --- a/py/config.py +++ b/py/config.py @@ -18,6 +18,18 @@ Configuration settings for PromptManager gallery and monitoring system. import os from typing import Dict, Any, List +# Import logging system +try: + from ..utils.logging_config import get_logger +except ImportError: + import sys + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger + +# Initialize logger for config operations +config_logger = get_logger('prompt_manager.config') + class GalleryConfig: """Configuration for the gallery monitoring system.""" @@ -127,11 +139,11 @@ class PromptManagerConfig: with open(config_path, 'r') as f: config = json.load(f) cls.update_config(config) - print(f"[PromptManager] Loaded configuration from {config_path}") + config_logger.info(f"Loaded configuration from {config_path}") except Exception as e: - print(f"[PromptManager] Error loading config from {config_path}: {e}") + config_logger.error(f"Error loading config from {config_path}: {e}") else: - print(f"[PromptManager] Config file not found: {config_path}, using defaults") + config_logger.info(f"Config file not found: {config_path}, using defaults") @classmethod def save_to_file(cls, config_path: str): @@ -145,9 +157,9 @@ class PromptManagerConfig: with open(config_path, 'w') as f: json.dump(config, f, indent=2) - print(f"[PromptManager] Saved configuration to {config_path}") + config_logger.info(f"Saved configuration to {config_path}") except Exception as e: - print(f"[PromptManager] Error saving config to {config_path}: {e}") + config_logger.error(f"Error saving config to {config_path}: {e}") @classmethod def update_config(cls, new_config: Dict[str, Any]): @@ -187,4 +199,4 @@ try: config_file = os.path.join(config_dir, 'config.json') PromptManagerConfig.load_from_file(config_file) except Exception as e: - print(f"[PromptManager] Error during config initialization: {e}") \ No newline at end of file + config_logger.error(f"Error during config initialization: {e}") \ No newline at end of file diff --git a/utils/diagnostics.py b/utils/diagnostics.py index 50d1a64..d3f2882 100644 --- a/utils/diagnostics.py +++ b/utils/diagnostics.py @@ -7,18 +7,21 @@ import sqlite3 from pathlib import Path from typing import Dict, Any, List +from .logging_config import get_logger + class GalleryDiagnostics: """Diagnostics for the gallery system.""" def __init__(self, db_path: str = "prompts.db"): self.db_path = db_path + self.logger = get_logger('prompt_manager.diagnostics') def run_full_diagnostic(self) -> Dict[str, Any]: """Run a complete diagnostic check.""" - print("\n" + "="*60) - print("๐ PROMPTMANAGER GALLERY DIAGNOSTICS") - print("="*60) + self.logger.info("\n" + "="*60) + self.logger.info("๐ PROMPTMANAGER GALLERY DIAGNOSTICS") + self.logger.info("="*60) results = { 'database': self.check_database(), @@ -28,22 +31,22 @@ class GalleryDiagnostics: 'dependencies': self.check_dependencies() } - print("\n" + "="*60) - print("๐ DIAGNOSTIC SUMMARY") - print("="*60) + self.logger.info("\n" + "="*60) + self.logger.info("๐ DIAGNOSTIC SUMMARY") + self.logger.info("="*60) for category, result in results.items(): status = "โ PASS" if result['status'] == 'ok' else "โ FAIL" - print(f"{category.upper():<20} {status}") + self.logger.info(f"{category.upper():<20} {status}") if result['status'] != 'ok': - print(f" Issue: {result['message']}") + self.logger.warning(f" Issue: {result['message']}") - print("\n" + "="*60) + self.logger.info("\n" + "="*60) return results def check_database(self) -> Dict[str, Any]: """Check database connection and structure.""" - print("\n๐๏ธ Checking Database...") + self.logger.info("\n๐๏ธ Checking Database...") try: if not os.path.exists(self.db_path): @@ -58,7 +61,7 @@ class GalleryDiagnostics: # Check prompts table cursor = conn.execute("SELECT COUNT(*) as count FROM prompts") prompt_count = cursor.fetchone()['count'] - print(f" ๐ Prompts in database: {prompt_count}") + self.logger.info(f" ๐ Prompts in database: {prompt_count}") # Check if generated_images table exists cursor = conn.execute(""" @@ -67,7 +70,7 @@ class GalleryDiagnostics: """) has_images_table = cursor.fetchone() is not None - print(f" ๐ผ๏ธ Images table exists: {has_images_table}") + self.logger.info(f" ๐ผ๏ธ Images table exists: {has_images_table}") return { 'status': 'ok', @@ -83,7 +86,7 @@ class GalleryDiagnostics: def check_images_table(self) -> Dict[str, Any]: """Check the generated_images table specifically.""" - print("\n๐ผ๏ธ Checking Images Table...") + self.logger.info("\n๐ผ๏ธ Checking Images Table...") try: with sqlite3.connect(self.db_path) as conn: @@ -104,7 +107,7 @@ class GalleryDiagnostics: # Check image records cursor = conn.execute("SELECT COUNT(*) as count FROM generated_images") image_count = cursor.fetchone()['count'] - print(f" ๐ Images in database: {image_count}") + self.logger.info(f" ๐ Images in database: {image_count}") # Get recent images cursor = conn.execute(""" @@ -116,9 +119,9 @@ class GalleryDiagnostics: """) recent_images = [dict(row) for row in cursor.fetchall()] - print(f" ๐ Recent images: {len(recent_images)}") + self.logger.info(f" ๐ Recent images: {len(recent_images)}") for img in recent_images: - print(f" - {img['filename']} -> Prompt {img['prompt_id']}") + self.logger.info(f" - {img['filename']} -> Prompt {img['prompt_id']}") return { 'status': 'ok', @@ -134,12 +137,12 @@ class GalleryDiagnostics: def check_file_system(self) -> Dict[str, Any]: """Check file system and permissions.""" - print("\n๐ Checking File System...") + self.logger.info("\n๐ Checking File System...") try: # Check current directory current_dir = os.getcwd() - print(f" ๐ Current directory: {current_dir}") + self.logger.info(f" ๐ Current directory: {current_dir}") # Check if we can write to current directory test_file = "test_write.tmp" @@ -151,7 +154,7 @@ class GalleryDiagnostics: except: can_write = False - print(f" โ๏ธ Can write to directory: {can_write}") + self.logger.info(f" โ๏ธ Can write to directory: {can_write}") return { 'status': 'ok', @@ -167,7 +170,7 @@ class GalleryDiagnostics: def check_comfyui_output(self) -> Dict[str, Any]: """Check ComfyUI output directories.""" - print("\n๐จ Checking ComfyUI Output...") + self.logger.info("\n๐จ Checking ComfyUI Output...") output_dirs = [] @@ -185,16 +188,16 @@ class GalleryDiagnostics: abs_path = os.path.abspath(dir_path) if os.path.exists(abs_path): output_dirs.append(abs_path) - print(f" ๐ Found output dir: {abs_path}") + self.logger.info(f" ๐ Found output dir: {abs_path}") # Count images in this directory try: image_files = [] for ext in ['.png', '.jpg', '.jpeg', '.webp']: image_files.extend(Path(abs_path).rglob(f'*{ext}')) - print(f" ๐ผ๏ธ Images found: {len(image_files)}") + self.logger.info(f" ๐ผ๏ธ Images found: {len(image_files)}") except Exception as e: - print(f" โ Error scanning: {e}") + self.logger.error(f" โ Error scanning: {e}") # Try ComfyUI's folder_paths try: @@ -202,9 +205,9 @@ class GalleryDiagnostics: comfyui_output = folder_paths.get_output_directory() if comfyui_output and comfyui_output not in output_dirs: output_dirs.append(comfyui_output) - print(f" ๐ ComfyUI output dir: {comfyui_output}") + self.logger.info(f" ๐ ComfyUI output dir: {comfyui_output}") except ImportError: - print(" โ ๏ธ ComfyUI folder_paths not available") + self.logger.warning(" โ ๏ธ ComfyUI folder_paths not available") return { 'status': 'ok' if output_dirs else 'warning', @@ -214,7 +217,7 @@ class GalleryDiagnostics: def check_dependencies(self) -> Dict[str, Any]: """Check required dependencies.""" - print("\n๐ฆ Checking Dependencies...") + self.logger.info("\n๐ฆ Checking Dependencies...") dependencies = { 'watchdog': False, @@ -226,25 +229,25 @@ class GalleryDiagnostics: try: import watchdog dependencies['watchdog'] = True - print(f" โ watchdog: {watchdog.__version__}") + self.logger.info(f" โ watchdog: {watchdog.__version__}") except ImportError: - print(" โ watchdog: NOT INSTALLED") + self.logger.error(" โ watchdog: NOT INSTALLED") # Check PIL try: from PIL import Image dependencies['PIL'] = True - print(f" โ PIL (Pillow): Available") + self.logger.info(f" โ PIL (Pillow): Available") except ImportError: - print(" โ PIL (Pillow): NOT AVAILABLE") + self.logger.error(" โ PIL (Pillow): NOT AVAILABLE") # Check sqlite3 try: import sqlite3 dependencies['sqlite3'] = True - print(f" โ sqlite3: {sqlite3.sqlite_version}") + self.logger.info(f" โ sqlite3: {sqlite3.sqlite_version}") except ImportError: - print(" โ sqlite3: NOT AVAILABLE") + self.logger.error(" โ sqlite3: NOT AVAILABLE") all_deps_ok = all(dependencies.values()) @@ -256,7 +259,7 @@ class GalleryDiagnostics: def create_test_image_link(self, prompt_id: int, test_image_path: str = None) -> Dict[str, Any]: """Create a test image link to verify the system works.""" - print(f"\n๐งช Creating test image link for prompt {prompt_id}...") + self.logger.info(f"\n๐งช Creating test image link for prompt {prompt_id}...") try: # Import database operations @@ -289,7 +292,7 @@ class GalleryDiagnostics: metadata=test_metadata ) - print(f" โ Test image linked with ID: {image_id}") + self.logger.info(f" โ Test image linked with ID: {image_id}") return { 'status': 'ok', diff --git a/utils/image_monitor.py b/utils/image_monitor.py index 95f98a5..b214539 100644 --- a/utils/image_monitor.py +++ b/utils/image_monitor.py @@ -13,6 +13,7 @@ from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from .metadata_extractor import ComfyUIMetadataExtractor +from .logging_config import get_logger class ImageGenerationHandler(FileSystemEventHandler): @@ -30,11 +31,12 @@ class ImageGenerationHandler(FileSystemEventHandler): self.prompt_tracker = prompt_tracker self.metadata_extractor = ComfyUIMetadataExtractor() self.processing_delay = 2.0 # Wait 2 seconds before processing + self.logger = get_logger('prompt_manager.image_monitor') def on_created(self, event): """Handle file creation events.""" if not event.is_directory and self.is_image_file(event.src_path): - print(f"[PromptManager] 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, @@ -49,24 +51,24 @@ class ImageGenerationHandler(FileSystemEventHandler): def process_new_image(self, image_path: str): """Process a newly created image file.""" try: - print(f"[PromptManager] Processing image: {image_path}") + self.logger.info(f"Processing image: {image_path}") if not os.path.exists(image_path): - print(f"[PromptManager] Image file no longer 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() - print(f"[PromptManager] Current prompt context: {current_prompt['id'] if current_prompt else 'None'}") + self.logger.debug(f"Current prompt context: {current_prompt['id'] if current_prompt else 'None'}") if not current_prompt: - print(f"[PromptManager] No active prompt context for image: {image_path}") + self.logger.info(f"No active prompt context for image: {image_path}") # Fallback: try to link to the most recent prompt in database current_prompt = self._get_fallback_prompt() if current_prompt: - print(f"[PromptManager] Using fallback prompt: {current_prompt['id']}") + self.logger.info(f"Using fallback prompt: {current_prompt['id']}") else: - print(f"[PromptManager] No fallback prompt available, skipping image") + self.logger.warning(f"No fallback prompt available, skipping image") return else: # Extend the timeout for this prompt since we're still getting images @@ -76,24 +78,24 @@ class ImageGenerationHandler(FileSystemEventHandler): # Extract ComfyUI metadata try: metadata = self.metadata_extractor.extract_metadata(image_path) - print(f"[PromptManager] Extracted metadata: {bool(metadata)}") + self.logger.debug(f"Extracted metadata: {bool(metadata)}") except Exception as meta_error: - print(f"[PromptManager] Metadata extraction failed: {meta_error}") + self.logger.warning(f"Metadata extraction failed: {meta_error}") metadata = None if metadata: - print(f"[PromptManager] Linking image with full metadata to prompt {current_prompt['id']}") + self.logger.info(f"Linking image with full metadata to prompt {current_prompt['id']}") self.link_image_to_prompt(image_path, current_prompt, metadata) else: - print(f"[PromptManager] Linking image with basic info to prompt {current_prompt['id']}") + self.logger.info(f"Linking image with basic info to prompt {current_prompt['id']}") # Link with basic file info even without metadata basic_metadata = self.get_basic_file_info(image_path) self.link_image_to_prompt(image_path, current_prompt, {'file_info': basic_metadata}) except Exception as e: - print(f"[PromptManager] Error processing image {image_path}: {e}") + self.logger.error(f"Error processing image {image_path}: {e}") import traceback - traceback.print_exc() + self.logger.error(traceback.format_exc()) def get_basic_file_info(self, image_path: str) -> Dict[str, Any]: """Get basic file information when metadata extraction fails.""" @@ -117,7 +119,7 @@ class ImageGenerationHandler(FileSystemEventHandler): return file_info except Exception as e: - print(f"[PromptManager] Error getting file info: {e}") + self.logger.error(f"Error getting file info: {e}") return {} def _get_fallback_prompt(self) -> Optional[Dict[str, Any]]: @@ -133,7 +135,7 @@ class ImageGenerationHandler(FileSystemEventHandler): 'fallback': True } except Exception as e: - print(f"[PromptManager] Error getting fallback prompt: {e}") + self.logger.error(f"Error getting fallback prompt: {e}") return None def link_image_to_prompt(self, image_path: str, prompt_context: Dict, metadata: Dict): @@ -145,9 +147,9 @@ class ImageGenerationHandler(FileSystemEventHandler): metadata=metadata ) fallback_note = " (fallback)" if prompt_context.get('fallback') else "" - print(f"[PromptManager] Successfully linked image {image_id} to prompt {prompt_context['id']}{fallback_note}") + self.logger.info(f"Successfully linked image {image_id} to prompt {prompt_context['id']}{fallback_note}") except Exception as e: - print(f"[PromptManager] Failed to link image to prompt: {e}") + self.logger.error(f"Failed to link image to prompt: {e}") class ImageMonitor: @@ -166,6 +168,7 @@ class ImageMonitor: self.observer = None self.handler = None self.monitored_directories = [] + self.logger = get_logger('prompt_manager.image_monitor') def start_monitoring(self, output_directories: Optional[list] = None): """ @@ -175,7 +178,7 @@ class ImageMonitor: output_directories: List of directories to monitor. If None, auto-detect. """ if self.observer: - print("[PromptManager] Image monitoring already running") + self.logger.warning("Image monitoring already running") return # Auto-detect ComfyUI output directory if none provided @@ -183,7 +186,7 @@ class ImageMonitor: output_directories = self.detect_comfyui_output_dirs() if not output_directories: - print("[PromptManager] No output directories found to monitor") + self.logger.warning("No output directories found to monitor") return # Create event handler @@ -196,15 +199,15 @@ class ImageMonitor: if os.path.exists(output_dir): self.observer.schedule(self.handler, output_dir, recursive=True) self.monitored_directories.append(output_dir) - print(f"[PromptManager] Monitoring directory: {output_dir}") + self.logger.info(f"Monitoring directory: {output_dir}") else: - print(f"[PromptManager] Directory does not exist: {output_dir}") + self.logger.warning(f"Directory does not exist: {output_dir}") if self.monitored_directories: self.observer.start() - print(f"[PromptManager] Image monitoring started for {len(self.monitored_directories)} directories") + self.logger.info(f"Image monitoring started for {len(self.monitored_directories)} directories") else: - print("[PromptManager] No valid directories to monitor") + self.logger.warning("No valid directories to monitor") def stop_monitoring(self): """Stop the image monitoring system.""" @@ -214,7 +217,7 @@ class ImageMonitor: self.observer = None self.handler = None self.monitored_directories = [] - print("[PromptManager] Image monitoring stopped") + self.logger.info("Image monitoring stopped") def detect_comfyui_output_dirs(self) -> list: """Auto-detect ComfyUI output directories.""" @@ -226,9 +229,9 @@ class ImageMonitor: output_dir = folder_paths.get_output_directory() if output_dir and os.path.exists(output_dir): potential_dirs.append(output_dir) - print(f"[PromptManager] Detected ComfyUI output directory: {output_dir}") + self.logger.info(f"Detected ComfyUI output directory: {output_dir}") except ImportError: - print("[PromptManager] ComfyUI folder_paths not available, using fallback detection") + self.logger.debug("ComfyUI folder_paths not available, using fallback detection") # Fallback: Look for common ComfyUI directory structures fallback_paths = [ @@ -243,7 +246,7 @@ class ImageMonitor: abs_path = os.path.abspath(path) if os.path.exists(abs_path) and abs_path not in potential_dirs: potential_dirs.append(abs_path) - print(f"[PromptManager] Found output directory: {abs_path}") + self.logger.debug(f"Found output directory: {abs_path}") return potential_dirs diff --git a/utils/logging_config.py b/utils/logging_config.py new file mode 100644 index 0000000..ffa861e --- /dev/null +++ b/utils/logging_config.py @@ -0,0 +1,316 @@ +""" +Logging configuration for ComfyUI_PromptManager. + +This module provides centralized logging configuration with support for: +- Multiple log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) +- File rotation to prevent logs from growing too large +- Console and file output +- Structured logging with timestamps and context +- Log viewer API integration +""" + +import logging +import logging.handlers +import os +import json +import threading +from datetime import datetime +from typing import Dict, Any, Optional, List +from pathlib import Path + + +class PromptManagerLogger: + """ + Centralized logging system for PromptManager. + + Features: + - Configurable log levels + - File rotation with size limits + - Structured JSON logging for API consumption + - Thread-safe operations + - Memory buffer for recent logs + """ + + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if hasattr(self, '_initialized'): + return + + self._initialized = True + self.log_dir = Path(__file__).parent.parent / "logs" + self.log_dir.mkdir(exist_ok=True) + + # Configuration + self.config = { + 'level': 'INFO', + 'max_file_size': 10 * 1024 * 1024, # 10MB + 'backup_count': 5, + 'console_logging': True, + 'file_logging': True, + 'buffer_size': 1000 # Keep last 1000 log entries in memory + } + + # Memory buffer for recent logs (for web viewer) + self._log_buffer = [] + self._buffer_lock = threading.Lock() + + # Initialize loggers + self._setup_loggers() + + def _setup_loggers(self): + """Set up all loggers with appropriate handlers.""" + # Main logger + self.logger = logging.getLogger('prompt_manager') + self.logger.setLevel(getattr(logging, self.config['level'])) + + # Clear existing handlers + for handler in self.logger.handlers[:]: + self.logger.removeHandler(handler) + + # Custom formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Console handler + if self.config['console_logging']: + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + self.logger.addHandler(console_handler) + + # File handler with rotation + if self.config['file_logging']: + log_file = self.log_dir / "prompt_manager.log" + file_handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=self.config['max_file_size'], + backupCount=self.config['backup_count'] + ) + file_handler.setFormatter(formatter) + self.logger.addHandler(file_handler) + + # Memory handler for web viewer + memory_handler = MemoryBufferHandler(self) + memory_handler.setFormatter(formatter) + self.logger.addHandler(memory_handler) + + # Component-specific loggers + self._setup_component_loggers() + + def _setup_component_loggers(self): + """Set up loggers for specific components.""" + components = [ + 'prompt_manager.database', + 'prompt_manager.api', + 'prompt_manager.image_monitor', + 'prompt_manager.prompt_tracker', + 'prompt_manager.web_ui' + ] + + for component in components: + comp_logger = logging.getLogger(component) + comp_logger.setLevel(getattr(logging, self.config['level'])) + # Child loggers inherit handlers from parent + + def get_logger(self, name: str = 'prompt_manager') -> logging.Logger: + """Get a logger instance for a specific component.""" + return logging.getLogger(name) + + def add_to_buffer(self, record: logging.LogRecord, formatted_message: str): + """Add a log entry to the memory buffer for web viewer.""" + with self._buffer_lock: + log_entry = { + 'timestamp': datetime.fromtimestamp(record.created).isoformat(), + 'level': record.levelname, + 'logger': record.name, + 'message': record.getMessage(), + 'formatted': formatted_message, + 'module': record.module, + 'filename': record.filename, + 'lineno': record.lineno, + 'thread': record.thread, + 'thread_name': record.threadName if hasattr(record, 'threadName') else '', + 'process': record.process + } + + self._log_buffer.append(log_entry) + + # Maintain buffer size limit + if len(self._log_buffer) > self.config['buffer_size']: + self._log_buffer = self._log_buffer[-self.config['buffer_size']:] + + def get_recent_logs(self, limit: int = 100, level: Optional[str] = None) -> List[Dict[str, Any]]: + """Get recent log entries from memory buffer.""" + with self._buffer_lock: + logs = self._log_buffer[:] + + # Filter by level if specified + if level: + level_num = getattr(logging, level.upper(), None) + if level_num: + logs = [log for log in logs if getattr(logging, log['level']) >= level_num] + + # Return most recent first + return list(reversed(logs[-limit:])) + + def get_log_files(self) -> List[Dict[str, Any]]: + """Get information about available log files.""" + log_files = [] + + for log_file in self.log_dir.glob("*.log*"): + try: + stat = log_file.stat() + log_files.append({ + 'filename': log_file.name, + 'path': str(log_file), + 'size': stat.st_size, + 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), + 'is_main': log_file.name == 'prompt_manager.log' + }) + except Exception as e: + self.logger.error(f"Error getting info for log file {log_file}: {e}") + + # Sort by modification time, newest first + log_files.sort(key=lambda x: x['modified'], reverse=True) + return log_files + + def read_log_file(self, filename: str, lines: int = 100) -> List[str]: + """Read lines from a specific log file.""" + log_file = self.log_dir / filename + + if not log_file.exists() or not log_file.is_file(): + raise FileNotFoundError(f"Log file not found: {filename}") + + # Security check - ensure file is in log directory + if not str(log_file.resolve()).startswith(str(self.log_dir.resolve())): + raise ValueError("Invalid log file path") + + try: + with open(log_file, 'r', encoding='utf-8') as f: + all_lines = f.readlines() + return all_lines[-lines:] if lines > 0 else all_lines + except Exception as e: + self.logger.error(f"Error reading log file {filename}: {e}") + raise + + def truncate_logs(self) -> Dict[str, Any]: + """Truncate all log files.""" + results = { + 'truncated': [], + 'errors': [] + } + + for log_file in self.log_dir.glob("prompt_manager.log*"): + try: + if log_file.name == 'prompt_manager.log': + # For main log file, just clear it + with open(log_file, 'w') as f: + f.write("") + else: + # For rotated files, delete them + log_file.unlink() + + results['truncated'].append(log_file.name) + self.logger.info(f"Truncated log file: {log_file.name}") + + except Exception as e: + error_msg = f"Failed to truncate {log_file.name}: {str(e)}" + results['errors'].append(error_msg) + self.logger.error(error_msg) + + # Clear memory buffer + with self._buffer_lock: + self._log_buffer.clear() + + return results + + def update_config(self, new_config: Dict[str, Any]): + """Update logging configuration.""" + self.config.update(new_config) + + # Reconfigure loggers if level changed + if 'level' in new_config: + level = getattr(logging, new_config['level'].upper(), logging.INFO) + self.logger.setLevel(level) + + # Update all component loggers + for logger_name in logging.Logger.manager.loggerDict: + if logger_name.startswith('prompt_manager'): + logger = logging.getLogger(logger_name) + logger.setLevel(level) + + # Re-setup loggers if handlers changed + if any(key in new_config for key in ['console_logging', 'file_logging', 'max_file_size', 'backup_count']): + self._setup_loggers() + + self.logger.info(f"Updated logging configuration: {new_config}") + + def get_config(self) -> Dict[str, Any]: + """Get current logging configuration.""" + return self.config.copy() + + def get_log_stats(self) -> Dict[str, Any]: + """Get logging statistics.""" + with self._buffer_lock: + buffer_count = len(self._log_buffer) + level_counts = {} + for entry in self._log_buffer: + level = entry['level'] + level_counts[level] = level_counts.get(level, 0) + 1 + + log_files = self.get_log_files() + total_log_size = sum(f['size'] for f in log_files) + + return { + 'buffer_count': buffer_count, + 'level_counts': level_counts, + 'log_files_count': len(log_files), + 'total_log_size': total_log_size, + 'log_directory': str(self.log_dir), + 'current_level': self.config['level'] + } + + +class MemoryBufferHandler(logging.Handler): + """Custom logging handler that stores logs in memory for web viewer.""" + + def __init__(self, logger_manager: PromptManagerLogger): + super().__init__() + self.logger_manager = logger_manager + + def emit(self, record: logging.LogRecord): + """Handle a log record by adding it to the memory buffer.""" + try: + formatted_message = self.format(record) + self.logger_manager.add_to_buffer(record, formatted_message) + except Exception: + # Don't let logging errors break the application + pass + + +# Global logger instance +_logger_manager = None + +def get_logger_manager() -> PromptManagerLogger: + """Get the global logger manager instance.""" + global _logger_manager + if _logger_manager is None: + _logger_manager = PromptManagerLogger() + return _logger_manager + +def get_logger(name: str = 'prompt_manager') -> logging.Logger: + """Convenience function to get a logger.""" + return get_logger_manager().get_logger(name) + +# Initialize logging on import +get_logger_manager() \ No newline at end of file diff --git a/utils/metadata_extractor.py b/utils/metadata_extractor.py index c01bf2c..632f125 100644 --- a/utils/metadata_extractor.py +++ b/utils/metadata_extractor.py @@ -9,13 +9,15 @@ from typing import Optional, Dict, Any from PIL import Image from PIL.PngImagePlugin import PngInfo +from .logging_config import get_logger + class ComfyUIMetadataExtractor: """Extracts ComfyUI metadata from generated images.""" def __init__(self): """Initialize the metadata extractor.""" - pass + self.logger = get_logger('prompt_manager.metadata_extractor') def extract_metadata(self, image_path: str) -> Optional[Dict[str, Any]]: """ @@ -48,7 +50,7 @@ class ComfyUIMetadataExtractor: metadata['text_encoder_nodes'] = text_encoder_nodes except json.JSONDecodeError as e: - print(f"[PromptManager] Failed to parse workflow JSON: {e}") + self.logger.warning(f"Failed to parse workflow JSON: {e}") # Look for prompt data if 'prompt' in image.text: @@ -56,7 +58,7 @@ class ComfyUIMetadataExtractor: prompt_data = json.loads(image.text['prompt']) metadata['prompt'] = prompt_data except json.JSONDecodeError as e: - print(f"[PromptManager] Failed to parse prompt JSON: {e}") + self.logger.warning(f"Failed to parse prompt JSON: {e}") # Extract other common metadata fields metadata_fields = [ @@ -76,7 +78,7 @@ class ComfyUIMetadataExtractor: return metadata if any(key != 'file_info' for key in metadata.keys()) else None except Exception as e: - print(f"[PromptManager] Error extracting metadata from {image_path}: {e}") + self.logger.error(f"Error extracting metadata from {image_path}: {e}") return None def get_file_info(self, image_path: str, image: Image.Image) -> Dict[str, Any]: @@ -101,7 +103,7 @@ class ComfyUIMetadataExtractor: 'modified_time': stat.st_mtime } except Exception as e: - print(f"[PromptManager] Error getting file info: {e}") + self.logger.error(f"Error getting file info: {e}") return {} def find_text_encoder_nodes(self, workflow_data: Dict) -> list: diff --git a/utils/prompt_tracker.py b/utils/prompt_tracker.py index 98785b6..80e8615 100644 --- a/utils/prompt_tracker.py +++ b/utils/prompt_tracker.py @@ -10,6 +10,16 @@ import hashlib from typing import Optional, Dict, Any from datetime import datetime, timezone +# Import logging system +try: + from .logging_config import get_logger +except ImportError: + import sys + import os + current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, current_dir) + from utils.logging_config import get_logger + class PromptTracker: """Thread-safe tracking of current prompt executions.""" @@ -21,6 +31,9 @@ class PromptTracker: Args: db_manager: Database manager instance """ + self.logger = get_logger('prompt_manager.prompt_tracker') + self.logger.info("Initializing PromptTracker") + self.db_manager = db_manager self._local = threading.local() self.active_prompts = {} # Global tracking for multiple threads @@ -31,6 +44,7 @@ class PromptTracker: # Start cleanup thread self.cleanup_thread = threading.Thread(target=self._cleanup_expired_prompts, daemon=True) self.cleanup_thread.start() + self.logger.debug("PromptTracker initialization completed") def set_current_prompt(self, prompt_text: str, additional_data: Optional[Dict[str, Any]] = None) -> str: """ @@ -61,13 +75,13 @@ class PromptTracker: if existing_prompt: prompt_id = existing_prompt['id'] - print(f"[PromptManager] Found existing prompt ID: {prompt_id}") + self.logger.debug(f"Found existing prompt ID: {prompt_id}") else: # Generate a temporary ID for tracking (prompt should be saved by PromptManager) prompt_id = f"temp_{int(time.time())}" - print(f"[PromptManager] Using temporary ID for tracking: {prompt_id}") + self.logger.debug(f"Using temporary ID for tracking: {prompt_id}") except Exception as e: - print(f"[PromptManager] Error finding prompt: {e}") + self.logger.error(f"Error finding prompt: {e}") prompt_id = f"temp_{int(time.time())}" # Create execution context @@ -87,8 +101,8 @@ class PromptTracker: with self.lock: self.active_prompts[execution_id] = execution_context - print(f"[PromptManager] Set current prompt: {execution_id} -> {prompt_text[:50]}... (thread: {threading.current_thread().ident})") - print(f"[PromptManager] Active prompts count: {len(self.active_prompts)}") + self.logger.debug(f"Set current prompt: {execution_id} -> {prompt_text[:50]}... (thread: {threading.current_thread().ident})") + self.logger.debug(f"Active prompts count: {len(self.active_prompts)}") return execution_id def get_current_prompt(self) -> Optional[Dict[str, Any]]: @@ -105,42 +119,42 @@ class PromptTracker: if time.time() - current['timestamp'] < self.prompt_timeout: return current else: - print(f"[PromptManager] Current prompt expired: {current['execution_id']}") + self.logger.debug(f"Current prompt expired: {current['execution_id']}") self.clear_current_prompt() # Fallback: try to find recent prompt from global tracking # This is crucial for image monitoring which runs in different threads recent_prompt = self._find_recent_prompt() if recent_prompt: - print(f"[PromptManager] Using recent prompt from global tracking: {recent_prompt['execution_id']}") + self.logger.debug(f"Using recent prompt from global tracking: {recent_prompt['execution_id']}") return recent_prompt - print(f"[PromptManager] No prompt context found (thread: {threading.current_thread().ident})") + self.logger.debug(f"No prompt context found (thread: {threading.current_thread().ident})") return None def _find_recent_prompt(self) -> Optional[Dict[str, Any]]: """Find the most recent prompt that's still valid.""" with self.lock: current_time = time.time() - print(f"[PromptManager] Searching for recent prompt among {len(self.active_prompts)} active prompts") + self.logger.debug(f"Searching for recent prompt among {len(self.active_prompts)} active prompts") recent_prompts = [] for exec_id, prompt in self.active_prompts.items(): age_seconds = current_time - prompt['timestamp'] - print(f"[PromptManager] Prompt {exec_id}: age={age_seconds:.1f}s, timeout={self.prompt_timeout}s") + self.logger.debug(f"Prompt {exec_id}: age={age_seconds:.1f}s, timeout={self.prompt_timeout}s") if age_seconds < self.prompt_timeout: recent_prompts.append(prompt) else: - print(f"[PromptManager] Prompt {exec_id} is expired") + self.logger.debug(f"Prompt {exec_id} is expired") if recent_prompts: # Return the most recent one most_recent = max(recent_prompts, key=lambda p: p['timestamp']) - print(f"[PromptManager] Found recent prompt: {most_recent['execution_id']}") + self.logger.debug(f"Found recent prompt: {most_recent['execution_id']}") return most_recent else: - print(f"[PromptManager] No recent prompts found") + self.logger.debug(f"No recent prompts found") return None @@ -149,7 +163,7 @@ class PromptTracker: current = getattr(self._local, 'current_prompt', None) if current: execution_id = current['execution_id'] - print(f"[PromptManager] Clearing current prompt: {execution_id} (thread: {threading.current_thread().ident})") + self.logger.debug(f"Clearing current prompt: {execution_id} (thread: {threading.current_thread().ident})") # Clear from thread-local storage self._local.current_prompt = None @@ -158,12 +172,12 @@ class PromptTracker: with self.lock: removed = self.active_prompts.pop(execution_id, None) if removed: - print(f"[PromptManager] Removed prompt from global tracking: {execution_id}") - print(f"[PromptManager] Remaining active prompts: {len(self.active_prompts)}") + self.logger.debug(f"Removed prompt from global tracking: {execution_id}") + self.logger.debug(f"Remaining active prompts: {len(self.active_prompts)}") else: - print(f"[PromptManager] Prompt {execution_id} was not in global tracking") + self.logger.debug(f"Prompt {execution_id} was not in global tracking") else: - print(f"[PromptManager] No current prompt to clear (thread: {threading.current_thread().ident})") + self.logger.debug(f"No current prompt to clear (thread: {threading.current_thread().ident})") def extend_prompt_timeout(self, execution_id: str, additional_seconds: int = 60): """ @@ -176,7 +190,7 @@ class PromptTracker: with self.lock: if execution_id in self.active_prompts: self.active_prompts[execution_id]['timestamp'] = time.time() - print(f"[PromptManager] Extended timeout for prompt: {execution_id}") + self.logger.debug(f"Extended timeout for prompt: {execution_id}") def generate_execution_id(self) -> str: """Generate a unique execution ID.""" @@ -198,12 +212,12 @@ class PromptTracker: self.active_prompts.pop(exec_id, None) if expired_ids: - print(f"[PromptManager] Cleaned up {len(expired_ids)} expired prompts") + self.logger.info(f"Cleaned up {len(expired_ids)} expired prompts") time.sleep(self.cleanup_interval) except Exception as e: - print(f"[PromptManager] Error in cleanup thread: {e}") + self.logger.error(f"Error in cleanup thread: {e}") time.sleep(60) # Wait a minute before retrying def get_active_prompts(self) -> Dict[str, Dict[str, Any]]: @@ -216,6 +230,23 @@ class PromptTracker: with self.lock: return self.active_prompts.copy() + def clear_all_active_prompts(self) -> int: + """ + Clear all active prompts (useful before scanning to avoid conflicts). + + Returns: + Number of prompts that were cleared + """ + with self.lock: + cleared_count = len(self.active_prompts) + self.active_prompts.clear() + + # Clear thread-local storage as well + self._local.current_prompt = None + + self.logger.info(f"Cleared {cleared_count} active prompts") + return cleared_count + def get_status(self) -> Dict[str, Any]: """ Get tracker status information. diff --git a/web/admin.html b/web/admin.html index 7a012c5..7dfba14 100644 --- a/web/admin.html +++ b/web/admin.html @@ -192,6 +192,10 @@ class="px-4 py-2 bg-pink-600 hover:bg-pink-700 text-white text-sm font-medium rounded-lg transition-colors"> ๐ธ Scan Images + @@ -602,6 +606,131 @@ + +
+