Merge pull request #11 from ComfyAssets/fixup_scanning

Fixup scanning and possible fixes for #8
This commit is contained in:
Vito
2025-05-28 08:35:14 -07:00
committed by GitHub
12 changed files with 1388 additions and 213 deletions
+27 -12
View File
@@ -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!")
log_message("✅ PromptManager custom node loaded successfully!")
+21 -10
View File
@@ -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
+24 -4
View File
@@ -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
+44 -19
View File
@@ -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."""
+366 -62
View File
@@ -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)
+18 -6
View File
@@ -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}")
config_logger.error(f"Error during config initialization: {e}")
+37 -34
View File
@@ -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',
+30 -27
View File
@@ -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
+316
View File
@@ -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()
+7 -5
View File
@@ -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:
+52 -21
View File
@@ -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.
+446 -13
View File
@@ -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
</button>
<button id="logsBtn"
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-lg transition-colors">
📋 View Logs
</button>
</div>
</div>
</div>
@@ -602,6 +606,131 @@
</div>
</div>
<!-- Logs Modal -->
<div id="logsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-gray-800 rounded-xl max-w-6xl w-full max-h-[90vh] overflow-hidden mx-4 border border-gray-700">
<div class="p-6 border-b border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-xl font-semibold text-gray-100">📋 System Logs</h3>
<button onclick="window.admin.closeLogs()" class="p-2 rounded-lg hover:bg-gray-700 transition-colors">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- Log Controls -->
<div class="p-6 border-b border-gray-700">
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Log Level</label>
<select id="logLevel" class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-100">
<option value="">All Levels</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARNING">Warning</option>
<option value="ERROR">Error</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Limit</label>
<select id="logLimit" class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-100">
<option value="50">Last 50</option>
<option value="100" selected>Last 100</option>
<option value="200">Last 200</option>
<option value="500">Last 500</option>
</select>
</div>
<div class="flex items-end">
<button id="refreshLogsBtn" class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
🔄 Refresh
</button>
</div>
<div class="flex items-end">
<button id="downloadLogsBtn" class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
📥 Download
</button>
</div>
<div class="flex items-end">
<button id="clearLogsBtn" class="w-full px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
🗑️ Clear Logs
</button>
</div>
</div>
<!-- Log Stats -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-4">
<div class="bg-gray-700 rounded-lg p-3">
<div class="text-xs text-gray-400">Total Logs</div>
<div class="text-lg font-semibold text-blue-400" id="logStatsTotal">-</div>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="text-xs text-gray-400">Errors</div>
<div class="text-lg font-semibold text-red-400" id="logStatsErrors">-</div>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="text-xs text-gray-400">Warnings</div>
<div class="text-lg font-semibold text-yellow-400" id="logStatsWarnings">-</div>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="text-xs text-gray-400">Current Level</div>
<div class="text-lg font-semibold text-green-400" id="logStatsLevel">-</div>
</div>
<div class="bg-gray-700 rounded-lg p-3">
<div class="text-xs text-gray-400">Log Size</div>
<div class="text-lg font-semibold text-purple-400" id="logStatsSize">-</div>
</div>
</div>
<!-- Log Settings -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Set Log Level</label>
<select id="setLogLevel" class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-100">
<option value="DEBUG">Debug</option>
<option value="INFO" selected>Info</option>
<option value="WARNING">Warning</option>
<option value="ERROR">Error</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
<div class="flex items-center">
<label class="flex items-center space-x-2 text-gray-300">
<input type="checkbox" id="consoleLogging" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500">
<span class="text-sm">Console Logging</span>
</label>
</div>
<div class="flex items-end">
<button id="updateLogConfigBtn" class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors">
💾 Update Config
</button>
</div>
</div>
</div>
<!-- Log Viewer -->
<div class="flex-1 overflow-hidden">
<div class="h-96 overflow-y-auto custom-scrollbar bg-gray-900 font-mono text-sm">
<div id="logsContainer" class="p-4 space-y-1">
<div class="text-center py-8 text-gray-500">
Click "Refresh" to load logs
</div>
</div>
</div>
</div>
<!-- Log Files -->
<div class="p-6 border-t border-gray-700">
<h4 class="text-lg font-medium text-gray-100 mb-4">Log Files</h4>
<div id="logFilesList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<!-- Log files will be inserted here -->
</div>
</div>
</div>
</div>
<script>
class PromptAdmin {
constructor() {
@@ -646,6 +775,7 @@
document.getElementById("backupBtn").addEventListener("click", () => this.backupDatabase());
document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal());
document.getElementById("logsBtn").addEventListener("click", () => this.showLogsModal());
// Modals
this.bindModalEvents();
@@ -687,6 +817,12 @@
document.getElementById("cancelScan").addEventListener("click", () => this.hideModal("scanModal"));
document.getElementById("quickBackupBtn").addEventListener("click", () => this.quickBackup());
// Logs modal
document.getElementById("refreshLogsBtn").addEventListener("click", () => this.refreshLogs());
document.getElementById("downloadLogsBtn").addEventListener("click", () => this.downloadLogs());
document.getElementById("clearLogsBtn").addEventListener("click", () => this.clearLogs());
document.getElementById("updateLogConfigBtn").addEventListener("click", () => this.updateLogConfig());
// Close modals on backdrop click
document.querySelectorAll("[id$='Modal']").forEach((modal) => {
modal.addEventListener("click", (e) => {
@@ -806,8 +942,7 @@
if (data.success) {
this.prompts = data.results;
this.renderPrompts();
// Update stats based on loaded data
this.updateLocalStats();
// Don't update stats from local data as it's only a subset of prompts
}
}
} catch (error) {
@@ -819,8 +954,12 @@
}
updateLocalStats() {
// Fallback stats calculation from loaded data
if (this.prompts.length > 0) {
// Only update local stats if we haven't loaded proper statistics yet
// This prevents overwriting the correct total counts from the API
const currentTotal = document.getElementById("totalPrompts").textContent;
// Only update if the current total is still the default "-" or if we have more prompts locally than the API count
if (currentTotal === "-" || (this.prompts.length > parseInt(currentTotal) && this.prompts.length > 0)) {
document.getElementById("totalPrompts").textContent = this.prompts.length;
const uniqueCategories = new Set(this.prompts.map(p => p.category).filter(Boolean));
@@ -1092,8 +1231,10 @@
this.prompts[promptIndex].rating = rating;
}
// Update stats
this.updateLocalStats();
// Refresh stats
setTimeout(() => {
this.loadStatistics();
}, 500);
} else {
// Revert UI change on API failure
const originalPrompt = this.prompts.find(p => p.id === promptId);
@@ -1175,6 +1316,11 @@
if (response.ok) {
this.showNotification("Prompt updated", "success");
// Refresh the page to show updated content
setTimeout(() => {
this.loadStatistics();
setTimeout(() => window.location.reload(), 500);
}, 1000);
} else {
throw new Error("Update failed");
}
@@ -1211,7 +1357,11 @@
if (response.ok) {
this.showNotification("Prompt deleted", "success");
this.search();
// Refresh stats and reload page
setTimeout(() => {
this.loadStatistics();
setTimeout(() => window.location.reload(), 500);
}, 1000);
}
} catch (error) {
this.showNotification("Failed to delete prompt", "error");
@@ -1290,7 +1440,11 @@
if (response.ok) {
this.showNotification(`${this.selectedPrompts.size} prompts deleted`, "success");
this.search();
// Refresh stats and reload page
setTimeout(() => {
this.loadStatistics();
setTimeout(() => window.location.reload(), 500);
}, 1000);
}
} catch (error) {
this.showNotification("Failed to delete prompts", "error");
@@ -2136,7 +2290,7 @@
data.found
);
} else if (data.type === 'complete') {
this.completeScan(data.processed, data.found, data.added);
this.completeScan(data.processed, data.found, data.added, data.linked);
return;
} else if (data.type === 'error') {
throw new Error(data.message);
@@ -2161,18 +2315,297 @@
document.getElementById("scanFound").textContent = `${found} prompts found`;
}
completeScan(processed, found, added) {
completeScan(processed, found, added, linked = 0) {
this.updateScanProgress(100, "Scan completed!", processed, found);
document.getElementById("startScan").disabled = false;
document.getElementById("startScan").textContent = "Start New Scan";
// Show detailed notification with all counts
const linkedText = linked > 0 ? `, linked ${linked} images to existing prompts` : '';
this.showNotification(
`Scan completed! Processed ${processed} files, found ${found} prompts, added ${added} new prompts to database.`,
`Scan completed! Processed ${processed} files, found ${found} prompts, added ${added} new prompts to database${linkedText}.`,
"success"
);
// Refresh the data
this.loadInitialData();
// Auto-close modal after a short delay to let user see the completion message
setTimeout(() => {
this.hideModal("scanModal");
// Refresh the statistics immediately
this.loadStatistics();
// Also reload the page after a short delay to ensure everything is fully updated
setTimeout(() => {
window.location.reload();
}, 1000);
}, 2000);
}
// Logs functionality
showLogsModal() {
this.showModal("logsModal");
this.loadLogStats();
this.loadLogFiles();
this.loadLogConfig();
}
closeLogs() {
this.hideModal("logsModal");
}
async loadLogStats() {
try {
const response = await fetch('/prompt_manager/logs/stats');
const data = await response.json();
if (data.success) {
const stats = data.stats;
document.getElementById("logStatsTotal").textContent = stats.buffer_count || 0;
document.getElementById("logStatsErrors").textContent = stats.level_counts?.ERROR || 0;
document.getElementById("logStatsWarnings").textContent = stats.level_counts?.WARNING || 0;
document.getElementById("logStatsLevel").textContent = stats.current_level || "INFO";
document.getElementById("logStatsSize").textContent = this.formatBytes(stats.total_log_size || 0);
}
} catch (error) {
console.error("Failed to load log stats:", error);
}
}
async loadLogConfig() {
try {
const response = await fetch('/prompt_manager/logs/config');
const data = await response.json();
if (data.success) {
const config = data.config;
document.getElementById("setLogLevel").value = config.level || "INFO";
document.getElementById("consoleLogging").checked = config.console_logging !== false;
}
} catch (error) {
console.error("Failed to load log config:", error);
}
}
async loadLogFiles() {
try {
const response = await fetch('/prompt_manager/logs/files');
const data = await response.json();
if (data.success) {
const container = document.getElementById("logFilesList");
if (data.files.length === 0) {
container.innerHTML = '<div class="col-span-full text-center text-gray-500">No log files found</div>';
return;
}
container.innerHTML = data.files.map(file => `
<div class="bg-gray-700 rounded-lg p-3">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-200">${file.filename}</span>
${file.is_main ? '<span class="bg-blue-500 text-xs px-2 py-1 rounded">Active</span>' : ''}
</div>
<div class="text-xs text-gray-400 mb-2">
Size: ${this.formatBytes(file.size)} | Modified: ${new Date(file.modified).toLocaleString()}
</div>
<button onclick="window.admin.downloadLogFile('${file.filename}')"
class="w-full px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-xs rounded transition-colors">
📥 Download
</button>
</div>
`).join('');
}
} catch (error) {
console.error("Failed to load log files:", error);
}
}
async refreshLogs() {
const level = document.getElementById("logLevel").value;
const limit = parseInt(document.getElementById("logLimit").value);
try {
const url = new URL('/prompt_manager/logs', window.location.origin);
if (level) url.searchParams.set('level', level);
url.searchParams.set('limit', limit.toString());
const response = await fetch(url);
const data = await response.json();
if (data.success) {
this.displayLogs(data.logs);
this.showNotification(`Loaded ${data.logs.length} log entries`, "success");
} else {
this.showNotification(`Failed to load logs: ${data.error}`, "error");
}
} catch (error) {
console.error("Failed to refresh logs:", error);
this.showNotification("Failed to refresh logs", "error");
}
}
displayLogs(logs) {
const container = document.getElementById("logsContainer");
if (logs.length === 0) {
container.innerHTML = '<div class="text-center py-8 text-gray-500">No logs found</div>';
return;
}
container.innerHTML = logs.map(log => {
const levelColors = {
DEBUG: 'text-gray-400',
INFO: 'text-blue-400',
WARNING: 'text-yellow-400',
ERROR: 'text-red-400',
CRITICAL: 'text-red-600'
};
const levelColor = levelColors[log.level] || 'text-gray-400';
const timestamp = new Date(log.timestamp).toLocaleString();
return `
<div class="border-l-2 border-gray-600 pl-3 py-1 hover:bg-gray-800 transition-colors">
<div class="flex items-start space-x-2 text-sm">
<span class="text-gray-500 text-xs font-mono w-24 flex-shrink-0">${timestamp.split(' ')[1]}</span>
<span class="${levelColor} font-semibold w-16 flex-shrink-0">${log.level}</span>
<span class="text-purple-400 text-xs w-32 flex-shrink-0">${log.logger}</span>
<span class="text-gray-300 flex-1">${log.message}</span>
</div>
<div class="text-xs text-gray-500 ml-44">
${log.filename}:${log.lineno}
</div>
</div>
`;
}).join('');
// Auto-scroll to bottom
container.scrollTop = container.scrollHeight;
}
async downloadLogs() {
const level = document.getElementById("logLevel").value;
const limit = parseInt(document.getElementById("logLimit").value);
try {
const url = new URL('/prompt_manager/logs', window.location.origin);
if (level) url.searchParams.set('level', level);
url.searchParams.set('limit', limit.toString());
const response = await fetch(url);
const data = await response.json();
if (data.success) {
const logText = data.logs.map(log =>
`${log.timestamp} [${log.level}] ${log.logger}: ${log.message} (${log.filename}:${log.lineno})`
).join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url2 = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url2;
a.download = `prompt_manager_logs_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url2);
this.showNotification("Logs downloaded successfully", "success");
} else {
this.showNotification(`Failed to download logs: ${data.error}`, "error");
}
} catch (error) {
console.error("Failed to download logs:", error);
this.showNotification("Failed to download logs", "error");
}
}
async downloadLogFile(filename) {
try {
const response = await fetch(`/prompt_manager/logs/download/${encodeURIComponent(filename)}`);
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification(`Downloaded ${filename}`, "success");
} else {
this.showNotification(`Failed to download ${filename}`, "error");
}
} catch (error) {
console.error("Failed to download log file:", error);
this.showNotification("Failed to download log file", "error");
}
}
async clearLogs() {
if (!confirm("Are you sure you want to clear all log files? This cannot be undone.")) {
return;
}
try {
const response = await fetch('/prompt_manager/logs/truncate', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, "success");
this.refreshLogs();
this.loadLogStats();
this.loadLogFiles();
} else {
this.showNotification(`Failed to clear logs: ${data.error}`, "error");
}
} catch (error) {
console.error("Failed to clear logs:", error);
this.showNotification("Failed to clear logs", "error");
}
}
async updateLogConfig() {
const level = document.getElementById("setLogLevel").value;
const consoleLogging = document.getElementById("consoleLogging").checked;
try {
const response = await fetch('/prompt_manager/logs/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
level: level,
console_logging: consoleLogging
})
});
const data = await response.json();
if (data.success) {
this.showNotification("Log configuration updated", "success");
this.loadLogStats();
} else {
this.showNotification(`Failed to update config: ${data.error}`, "error");
}
} catch (error) {
console.error("Failed to update log config:", error);
this.showNotification("Failed to update log configuration", "error");
}
}
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
}