From 7d65ee5516fbbcb3f511ed4003056075d83fe747 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 7 Oct 2026 18:45:04 -0700 Subject: [PATCH] refactor: one prompt hash implementation utils.hashing.generate_prompt_hash is now the only prompt hash: PromptManagerBase._generate_hash delegates to it and the inline hashlib.sha256 copies in the prompt tracker and image monitor are gone. Remove PromptManagerBase._open_web_interface (it targeted a web_interface folder that never existed) and the unused restart_gallery.py script. IS_CHANGED on PromptManager and PromptManagerText now includes category and tags, so editing either re-runs the node and updates the saved metadata. --- prompt_manager.py | 7 +-- prompt_manager_base.py | 40 +---------------- prompt_manager_text.py | 7 +-- restart_gallery.py | 64 --------------------------- tests/test_hashing.py | 44 ++++++++++++++++++ tests/test_node_execution_contract.py | 19 ++++++++ utils/image_monitor.py | 9 ++-- utils/prompt_tracker.py | 13 ++---- 8 files changed, 81 insertions(+), 122 deletions(-) delete mode 100644 restart_gallery.py diff --git a/prompt_manager.py b/prompt_manager.py index 9637c7c..50f3ffb 100644 --- a/prompt_manager.py +++ b/prompt_manager.py @@ -251,10 +251,11 @@ class PromptManager(PromptManagerBase, ComfyNodeABC): """ ComfyUI method to determine if node needs re-execution. - Returns a hash of input values that affect the conditioning output. - This enables proper branch execution - only re-execute when inputs change. + Returns a hash of input values that affect the conditioning output or + the saved prompt's metadata. This enables proper branch execution - + only re-execute when inputs change. """ import hashlib - combined = f"{text}|{prepend_text}|{append_text}" + combined = f"{text}|{prepend_text}|{append_text}|{category}|{tags}" return hashlib.sha256(combined.encode()).hexdigest() diff --git a/prompt_manager_base.py b/prompt_manager_base.py index ee190d5..2c8550f 100644 --- a/prompt_manager_base.py +++ b/prompt_manager_base.py @@ -6,9 +6,7 @@ search, gallery system management, and cleanup — extracted from the duplicate code in prompt_manager.py and prompt_manager_text.py. """ -import hashlib import os -import webbrowser from typing import Any, Dict, List, Optional try: @@ -225,16 +223,8 @@ class PromptManagerBase: return modified def _generate_hash(self, text: str) -> str: - """Generate SHA256 hash for the prompt text. - - Args: - text: The prompt text to hash - - Returns: - Hexadecimal string representation of the SHA256 hash - """ - normalized_text = text.strip().lower() - return hashlib.sha256(normalized_text.encode("utf-8")).hexdigest() + """The prompt's deduplication hash (see utils.hashing.generate_prompt_hash).""" + return generate_prompt_hash(text) def _parse_tags(self, tags_string: str) -> Optional[list]: """Parse comma-separated tags string into a list. @@ -285,32 +275,6 @@ class PromptManagerBase: self.logger.error(f"Error searching prompts: {e}") return [] - def _open_web_interface(self): - """Open the web interface in the default browser.""" - try: - current_dir = os.path.dirname(os.path.abspath(__file__)) - web_dir = os.path.join(current_dir, "web_interface") - - if os.path.exists(web_dir): - index_path = os.path.join(web_dir, "index.html") - if os.path.exists(index_path): - webbrowser.open(f"file://{index_path}") - self.logger.info("Web interface opened in browser") - else: - self.logger.warning( - f"Web interface directory found but no index.html. " - f"Please check {web_dir} for setup instructions" - ) - else: - 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: - 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.""" return self._search_prompts(search_text=search_text) diff --git a/prompt_manager_text.py b/prompt_manager_text.py index 37df856..f1f6ccd 100644 --- a/prompt_manager_text.py +++ b/prompt_manager_text.py @@ -223,10 +223,11 @@ class PromptManagerText(PromptManagerBase, ComfyNodeABC): """ ComfyUI method to determine if node needs re-execution. - Returns a hash of input values that affect the text output. - This enables proper branch execution - only re-execute when inputs change. + Returns a hash of input values that affect the text output or the + saved prompt's metadata. This enables proper branch execution - only + re-execute when inputs change. """ import hashlib - combined = f"{text}|{prepend_text}|{append_text}" + combined = f"{text}|{prepend_text}|{append_text}|{category}|{tags}" return hashlib.sha256(combined.encode()).hexdigest() diff --git a/restart_gallery.py b/restart_gallery.py deleted file mode 100644 index 7403594..0000000 --- a/restart_gallery.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -""" -ComfyUI_PromptManager Gallery System Restart Script. - -This script reinitializes and restarts the automatic image gallery monitoring system -for ComfyUI_PromptManager. Use this script after code modifications or when the -gallery system needs to be reloaded. - -The script performs the following operations: -1. Initializes the database connection -2. Creates a PromptTracker instance for linking prompts to images -3. Sets up the ImageMonitor for real-time file system monitoring -4. Starts monitoring ComfyUI output directories -5. Reports system status and readiness - -Usage: - python restart_gallery.py - -This is particularly useful during development when you need to restart the -gallery monitoring system without restarting the entire ComfyUI application. -""" - -import sys -import os - -# Add current directory to Python path -current_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, current_dir) - -try: - # Import and initialize the gallery system - from database.operations import PromptDatabase - from utils.prompt_tracker import get_prompt_tracker - from utils.image_monitor import get_image_monitor - - print("[RESTART] Restarting PromptManager Gallery System...") - - # Initialize components using singleton getters - db = PromptDatabase() - tracker = get_prompt_tracker(db) - monitor = get_image_monitor(db, tracker) - - print("[SUCCESS] Components initialized successfully") - - # Test image monitoring directories - status = monitor.get_status() - print(f"[INFO] Monitoring status: {status}") - - # Start monitoring - monitor.start_monitoring() - print("[SUCCESS] Image monitoring restarted") - - print("\n[READY] Gallery system restart completed!") - print("Generate some images now to test the automatic linking.") - -except ImportError as e: - print(f"[ERROR] Import error: {e}") - print("Make sure you're running this from the PromptManager directory") - -except Exception as e: - print(f"[ERROR] Error: {e}") - import traceback - - traceback.print_exc() diff --git a/tests/test_hashing.py b/tests/test_hashing.py index ed631fa..dc650f3 100644 --- a/tests/test_hashing.py +++ b/tests/test_hashing.py @@ -18,6 +18,50 @@ from utils.hashing import ( is_duplicate_prompt, ) +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +class TestSingleHashImplementation(unittest.TestCase): + """generate_prompt_hash is the only prompt hash; nothing re-implements it.""" + + def test_node_base_delegates_to_generate_prompt_hash(self): + from prompt_manager_base import PromptManagerBase + + node = PromptManagerBase.__new__(PromptManagerBase) + self.assertEqual(node._generate_hash(" A "), generate_prompt_hash(" A ")) + self.assertEqual(node._generate_hash(" A "), generate_prompt_hash("a")) + + def test_tracker_and_monitor_use_generate_prompt_hash(self): + for relative in ( + os.path.join("utils", "prompt_tracker.py"), + os.path.join("utils", "image_monitor.py"), + "prompt_manager_base.py", + ): + with open(os.path.join(ROOT, relative), encoding="utf-8") as f: + source = f.read() + self.assertFalse("hashlib.sha256" in source, f"inline hash in {relative}") + self.assertTrue("generate_prompt_hash" in source, relative) + + def test_tracker_finds_an_existing_prompt_by_the_shared_hash(self): + import unittest.mock as mock + + from utils.prompt_tracker import PromptTracker + + db = mock.Mock() + db.get_prompt_by_hash.return_value = {"id": 42} + tracker = PromptTracker(db) + tracker.set_current_prompt(" Hello World ") + db.get_prompt_by_hash.assert_called_once_with( + generate_prompt_hash(" Hello World ") + ) + self.assertEqual(tracker.get_current_prompt()["id"], 42) + + def test_node_base_has_no_browser_opener(self): + from prompt_manager_base import PromptManagerBase + + self.assertFalse(hasattr(PromptManagerBase, "_open_web_interface")) + self.assertFalse(os.path.exists(os.path.join(ROOT, "restart_gallery.py"))) + class TestGenerateContentHash(unittest.TestCase): """Test generate_content_hash function.""" diff --git a/tests/test_node_execution_contract.py b/tests/test_node_execution_contract.py index e7b5abb..05d3be0 100644 --- a/tests/test_node_execution_contract.py +++ b/tests/test_node_execution_contract.py @@ -70,6 +70,16 @@ class TestIsChangedPromptManager(unittest.TestCase): result = PromptManager.IS_CHANGED(clip=None, text="hello") self.assertIsInstance(result, str) + def test_different_tags_return_different_hash(self): + result1 = PromptManager.IS_CHANGED(clip=None, text="hello", tags="a, b") + result2 = PromptManager.IS_CHANGED(clip=None, text="hello", tags="a, c") + self.assertNotEqual(result1, result2) + + def test_different_category_returns_different_hash(self): + result1 = PromptManager.IS_CHANGED(clip=None, text="hello", category="art") + result2 = PromptManager.IS_CHANGED(clip=None, text="hello", category="photo") + self.assertNotEqual(result1, result2) + class TestIsChangedPromptManagerText(unittest.TestCase): """IS_CHANGED for PromptManagerText should behave identically to @@ -99,6 +109,15 @@ class TestIsChangedPromptManagerText(unittest.TestCase): result2 = PromptManagerText.IS_CHANGED(text="hello", append_text="b") self.assertNotEqual(result1, result2) + def test_different_tags_or_category_return_different_hash(self): + base = PromptManagerText.IS_CHANGED(text="hello", category="x", tags="a") + self.assertNotEqual( + base, PromptManagerText.IS_CHANGED(text="hello", category="x", tags="b") + ) + self.assertNotEqual( + base, PromptManagerText.IS_CHANGED(text="hello", category="y", tags="a") + ) + def test_returns_string(self): result = PromptManagerText.IS_CHANGED(text="hello") self.assertIsInstance(result, str) diff --git a/utils/image_monitor.py b/utils/image_monitor.py index 2fdc62d..9c8563b 100644 --- a/utils/image_monitor.py +++ b/utils/image_monitor.py @@ -31,6 +31,7 @@ from typing import Optional, Dict, Any, Tuple from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler +from .hashing import generate_prompt_hash from .metadata_extractor import ComfyUIMetadataExtractor from .logging_config import get_logger from .prompt_graph import resolve_text, run_prompt_nodes @@ -399,11 +400,9 @@ class ImageGenerationHandler(FileSystemEventHandler): # Look up by hash in database try: - import hashlib - - normalized = prompt_text.strip().lower() - prompt_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest() - existing = self.db_manager.get_prompt_by_hash(prompt_hash) + existing = self.db_manager.get_prompt_by_hash( + generate_prompt_hash(prompt_text) + ) if existing: self.logger.debug( f"Found DB prompt {existing['id']} from metadata text" diff --git a/utils/prompt_tracker.py b/utils/prompt_tracker.py index 91242dd..cd65109 100644 --- a/utils/prompt_tracker.py +++ b/utils/prompt_tracker.py @@ -28,12 +28,11 @@ Or using the context manager: import threading import time import uuid -import hashlib from typing import Optional, Dict, Any -from datetime import datetime, timezone # Import logging system try: + from .hashing import generate_prompt_hash from .logging_config import get_logger except ImportError: import sys @@ -41,6 +40,7 @@ except ImportError: current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, current_dir) + from utils.hashing import generate_prompt_hash from utils.logging_config import get_logger @@ -132,13 +132,8 @@ class PromptTracker: if not prompt_id: # Fallback: try to find existing prompt using consistent hash calculation try: - # Use consistent hash calculation (same as PromptManager) - import hashlib - - normalized_text = prompt_text.strip().lower() - prompt_hash = hashlib.sha256( - normalized_text.encode("utf-8") - ).hexdigest() + # Same hash as the nodes, so the lookup finds their saved prompt + prompt_hash = generate_prompt_hash(prompt_text) existing_prompt = self.db_manager.get_prompt_by_hash(prompt_hash) if existing_prompt: