Merge pull request #91 from ComfyAssets/feature/button
Feature/button fixes #88
This commit is contained in:
+28
@@ -82,6 +82,34 @@ except Exception as e:
|
||||
except:
|
||||
pass
|
||||
|
||||
# Start image monitoring globally at module import time
|
||||
# This ensures images are linked regardless of which node is used in the workflow
|
||||
try:
|
||||
from .database.operations import PromptDatabase
|
||||
from .utils.image_monitor import get_image_monitor
|
||||
from .utils.prompt_tracker import get_prompt_tracker
|
||||
from .utils.logging_config import get_logger
|
||||
|
||||
_init_logger = get_logger("prompt_manager.init")
|
||||
_init_logger.info("Starting global image monitoring system...")
|
||||
|
||||
# Initialize database and get singleton instances
|
||||
_global_db = PromptDatabase()
|
||||
_global_prompt_tracker = get_prompt_tracker(_global_db)
|
||||
_global_image_monitor = get_image_monitor(_global_db, _global_prompt_tracker)
|
||||
|
||||
# Start monitoring
|
||||
_global_image_monitor.start_monitoring()
|
||||
_init_logger.info("Global image monitoring system started successfully")
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
from .utils.logging_config import get_logger
|
||||
_init_logger = get_logger("prompt_manager.init")
|
||||
_init_logger.error(f"Failed to start image monitoring: {e}")
|
||||
except:
|
||||
print(f"[ComfyUI-PromptManager] Warning: Failed to start image monitoring: {e}")
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
# Print startup message with loaded tools
|
||||
|
||||
+96
-2
@@ -91,9 +91,13 @@ class PromptModel:
|
||||
workflow_data TEXT,
|
||||
prompt_metadata TEXT,
|
||||
parameters TEXT,
|
||||
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE,
|
||||
UNIQUE(prompt_id, filename)
|
||||
)
|
||||
""")
|
||||
|
||||
# Add unique constraint to existing databases (migration)
|
||||
self._migrate_add_unique_constraint(conn)
|
||||
|
||||
# Check if we need to migrate from old schema with workflow_name
|
||||
self._migrate_workflow_name_removal(conn)
|
||||
@@ -249,10 +253,100 @@ class PromptModel:
|
||||
self.logger.error(f"Foreign key migration error: {e}")
|
||||
# If migration fails, continue with existing schema
|
||||
|
||||
def _migrate_add_unique_constraint(self, conn: sqlite3.Connection) -> None:
|
||||
"""
|
||||
Add UNIQUE constraint on (prompt_id, filename) to prevent duplicate image entries.
|
||||
|
||||
This migration:
|
||||
1. Checks if the constraint already exists
|
||||
2. Removes duplicate entries (keeping the most recent)
|
||||
3. Recreates the table with the UNIQUE constraint
|
||||
|
||||
Args:
|
||||
conn: Active database connection
|
||||
"""
|
||||
try:
|
||||
# Check if the unique constraint already exists by looking at table info
|
||||
cursor = conn.execute("PRAGMA index_list(generated_images)")
|
||||
indexes = cursor.fetchall()
|
||||
|
||||
# Check if we have a unique index on prompt_id, filename
|
||||
has_unique_constraint = False
|
||||
for idx in indexes:
|
||||
if idx[2] == 1: # unique flag
|
||||
cursor = conn.execute(f"PRAGMA index_info({idx[1]})")
|
||||
columns = [col[2] for col in cursor.fetchall()]
|
||||
if 'prompt_id' in columns and 'filename' in columns:
|
||||
has_unique_constraint = True
|
||||
break
|
||||
|
||||
if has_unique_constraint:
|
||||
return # Already migrated
|
||||
|
||||
self.logger.info("Migrating database: adding UNIQUE constraint on (prompt_id, filename)")
|
||||
|
||||
# First, remove duplicates keeping only the most recent (highest id)
|
||||
conn.execute("""
|
||||
DELETE FROM generated_images
|
||||
WHERE id NOT IN (
|
||||
SELECT MAX(id) FROM generated_images
|
||||
GROUP BY prompt_id, filename
|
||||
)
|
||||
""")
|
||||
|
||||
duplicates_removed = conn.total_changes
|
||||
if duplicates_removed > 0:
|
||||
self.logger.info(f"Removed {duplicates_removed} duplicate image entries")
|
||||
|
||||
# Create new table with UNIQUE constraint
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS generated_images_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prompt_id INTEGER NOT NULL,
|
||||
image_path TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
generation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
file_size INTEGER,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
format TEXT,
|
||||
workflow_data TEXT,
|
||||
prompt_metadata TEXT,
|
||||
parameters TEXT,
|
||||
FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE,
|
||||
UNIQUE(prompt_id, filename)
|
||||
)
|
||||
""")
|
||||
|
||||
# Copy data
|
||||
conn.execute("""
|
||||
INSERT INTO generated_images_new
|
||||
(id, prompt_id, image_path, filename, generation_time, file_size,
|
||||
width, height, format, workflow_data, prompt_metadata, parameters)
|
||||
SELECT id, prompt_id, image_path, filename, generation_time, file_size,
|
||||
width, height, format, workflow_data, prompt_metadata, parameters
|
||||
FROM generated_images
|
||||
""")
|
||||
|
||||
# Drop old table and rename new one
|
||||
conn.execute("DROP TABLE generated_images")
|
||||
conn.execute("ALTER TABLE generated_images_new RENAME TO generated_images")
|
||||
|
||||
# Recreate indexes
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_prompt_images ON generated_images(prompt_id)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_image_path ON generated_images(image_path)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_generation_time ON generated_images(generation_time)")
|
||||
|
||||
self.logger.info("UNIQUE constraint migration completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"UNIQUE constraint migration error: {e}")
|
||||
# Continue with existing schema if migration fails
|
||||
|
||||
def migrate_database(self) -> None:
|
||||
"""
|
||||
Apply any pending database migrations.
|
||||
|
||||
|
||||
This method serves as an entry point for future schema migrations.
|
||||
Add new migration logic here as the database evolves.
|
||||
"""
|
||||
|
||||
+13
-7
@@ -654,11 +654,12 @@ class PromptDatabase:
|
||||
# Proceed with linking
|
||||
filename = os.path.basename(image_path)
|
||||
file_info = metadata.get('file_info', {}) if metadata else {}
|
||||
|
||||
|
||||
# Use INSERT OR IGNORE to skip duplicates (same prompt_id + filename)
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO generated_images
|
||||
(prompt_id, image_path, filename, file_size, width, height, format,
|
||||
INSERT OR IGNORE INTO generated_images
|
||||
(prompt_id, image_path, filename, file_size, width, height, format,
|
||||
workflow_data, prompt_metadata, parameters)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
@@ -676,6 +677,11 @@ class PromptDatabase:
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
if cursor.lastrowid == 0:
|
||||
self.logger.debug(f"Image {filename} already linked to prompt {prompt_id_int}")
|
||||
return 0
|
||||
|
||||
return cursor.lastrowid
|
||||
|
||||
except Exception as e:
|
||||
@@ -685,18 +691,18 @@ class PromptDatabase:
|
||||
def get_prompt_images(self, prompt_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all images associated with a prompt.
|
||||
|
||||
|
||||
Args:
|
||||
prompt_id: The prompt ID
|
||||
|
||||
|
||||
Returns:
|
||||
List of image records
|
||||
"""
|
||||
with self.model.get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT * FROM generated_images
|
||||
WHERE prompt_id = ?
|
||||
SELECT * FROM generated_images
|
||||
WHERE prompt_id = ?
|
||||
ORDER BY generation_time DESC
|
||||
""",
|
||||
(prompt_id,)
|
||||
|
||||
+21
-32
@@ -37,8 +37,8 @@ except ImportError:
|
||||
try:
|
||||
from .database.operations import PromptDatabase
|
||||
from .utils.comfyui_integration import get_comfyui_integration
|
||||
from .utils.image_monitor import ImageMonitor
|
||||
from .utils.prompt_tracker import PromptExecutionContext, PromptTracker
|
||||
from .utils.image_monitor import get_image_monitor
|
||||
from .utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker
|
||||
except ImportError:
|
||||
# For direct imports when not in a package
|
||||
import os
|
||||
@@ -47,8 +47,8 @@ except ImportError:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from database.operations import PromptDatabase
|
||||
from utils.comfyui_integration import get_comfyui_integration
|
||||
from utils.image_monitor import ImageMonitor
|
||||
from utils.prompt_tracker import PromptExecutionContext, PromptTracker
|
||||
from utils.image_monitor import get_image_monitor
|
||||
from utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker
|
||||
|
||||
|
||||
class PromptManager(ComfyNodeABC):
|
||||
@@ -65,8 +65,9 @@ class PromptManager(ComfyNodeABC):
|
||||
self.logger.debug("Initializing PromptManager node")
|
||||
|
||||
self.db = PromptDatabase()
|
||||
self.prompt_tracker = PromptTracker(self.db)
|
||||
self.image_monitor = ImageMonitor(self.db, self.prompt_tracker)
|
||||
# Use singleton getters to ensure only one tracker/monitor exists
|
||||
self.prompt_tracker = get_prompt_tracker(self.db)
|
||||
self.image_monitor = get_image_monitor(self.db, self.prompt_tracker)
|
||||
self.comfyui_integration = get_comfyui_integration()
|
||||
|
||||
# Start image monitoring automatically
|
||||
@@ -132,7 +133,7 @@ class PromptManager(ComfyNodeABC):
|
||||
"A conditioning containing the embedded text used to guide the diffusion model.",
|
||||
"The final combined text string (with prepend/append applied) that was encoded.",
|
||||
)
|
||||
FUNCTION = "encode"
|
||||
FUNCTION = "encode_prompt"
|
||||
CATEGORY = "🫶 ComfyAssets/🧠 Prompts"
|
||||
DESCRIPTION = (
|
||||
"Encodes a text prompt using a CLIP model into an embedding that can be used to guide "
|
||||
@@ -140,7 +141,7 @@ class PromptManager(ComfyNodeABC):
|
||||
"to a local SQLite database with optional metadata for search and retrieval."
|
||||
)
|
||||
|
||||
def encode(
|
||||
def encode_prompt(
|
||||
self,
|
||||
clip,
|
||||
text: str,
|
||||
@@ -168,11 +169,18 @@ class PromptManager(ComfyNodeABC):
|
||||
Raises:
|
||||
RuntimeError: If clip input is invalid
|
||||
"""
|
||||
# Debug: Log text being encoded - this should print EVERY time the node executes
|
||||
import time
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[PromptManager] encode_prompt() CALLED at {time.time()}")
|
||||
print(f"[PromptManager] text = {repr(text)[:100]}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Combine prepend, main text, and append text
|
||||
final_text = ""
|
||||
if prepend_text and prepend_text.strip():
|
||||
final_text += prepend_text.strip() + " "
|
||||
final_text += text
|
||||
final_text += text if text else ""
|
||||
if append_text and append_text.strip():
|
||||
final_text += " " + append_text.strip()
|
||||
|
||||
@@ -259,7 +267,7 @@ class PromptManager(ComfyNodeABC):
|
||||
},
|
||||
)
|
||||
|
||||
self.logger.debug("CLIP encoding completed successfully")
|
||||
self.logger.info(f"CLIP encoding completed, text: {repr(encoding_text)[:80]}")
|
||||
return (conditioning, encoding_text)
|
||||
|
||||
def _save_prompt_to_database(
|
||||
@@ -484,25 +492,6 @@ class PromptManager(ComfyNodeABC):
|
||||
"""
|
||||
self.cleanup_gallery_system()
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, text="", category="", tags="", search_text="",
|
||||
prepend_text="", append_text="", **kwargs):
|
||||
"""
|
||||
ComfyUI method to determine if node needs re-execution.
|
||||
|
||||
This method now properly tracks input changes to avoid unnecessary
|
||||
re-execution while still ensuring prompts are saved when inputs change.
|
||||
|
||||
Returns:
|
||||
A hash of the input values that changes when any input changes
|
||||
"""
|
||||
# Create a hash of all the text inputs that affect the output
|
||||
# This ensures the node only re-executes when inputs actually change
|
||||
import hashlib
|
||||
|
||||
# Combine all text inputs that affect the conditioning output
|
||||
combined = f"{text}|{category}|{tags}|{prepend_text}|{append_text}"
|
||||
|
||||
# Return a hash that will change when inputs change
|
||||
# Note: We don't include search_text as it doesn't affect the output conditioning
|
||||
return hashlib.sha256(combined.encode()).hexdigest()
|
||||
# NOTE: IS_CHANGED intentionally removed to match CLIPTextEncode behavior
|
||||
# ComfyUI's default caching (based on input values) should handle cache invalidation
|
||||
# The previous IS_CHANGED implementation was causing input/cache mismatch issues
|
||||
|
||||
+10
-28
@@ -37,8 +37,8 @@ except ImportError:
|
||||
try:
|
||||
from .database.operations import PromptDatabase
|
||||
from .utils.comfyui_integration import get_comfyui_integration
|
||||
from .utils.image_monitor import ImageMonitor
|
||||
from .utils.prompt_tracker import PromptExecutionContext, PromptTracker
|
||||
from .utils.image_monitor import get_image_monitor
|
||||
from .utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker
|
||||
except ImportError:
|
||||
# For direct imports when not in a package
|
||||
import os
|
||||
@@ -47,8 +47,8 @@ except ImportError:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from database.operations import PromptDatabase
|
||||
from utils.comfyui_integration import get_comfyui_integration
|
||||
from utils.image_monitor import ImageMonitor
|
||||
from utils.prompt_tracker import PromptExecutionContext, PromptTracker
|
||||
from utils.image_monitor import get_image_monitor
|
||||
from utils.prompt_tracker import PromptExecutionContext, get_prompt_tracker
|
||||
|
||||
|
||||
class PromptManagerText(ComfyNodeABC):
|
||||
@@ -67,8 +67,9 @@ class PromptManagerText(ComfyNodeABC):
|
||||
self.logger.debug("Initializing PromptManagerText node")
|
||||
|
||||
self.db = PromptDatabase()
|
||||
self.prompt_tracker = PromptTracker(self.db)
|
||||
self.image_monitor = ImageMonitor(self.db, self.prompt_tracker)
|
||||
# Use singleton getters to ensure only one tracker/monitor exists
|
||||
self.prompt_tracker = get_prompt_tracker(self.db)
|
||||
self.image_monitor = get_image_monitor(self.db, self.prompt_tracker)
|
||||
self.comfyui_integration = get_comfyui_integration()
|
||||
|
||||
# Start image monitoring automatically
|
||||
@@ -455,25 +456,6 @@ class PromptManagerText(ComfyNodeABC):
|
||||
"""
|
||||
self.cleanup_gallery_system()
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, text="", category="", tags="", search_text="",
|
||||
prepend_text="", append_text="", **kwargs):
|
||||
"""
|
||||
ComfyUI method to determine if node needs re-execution.
|
||||
|
||||
This method now properly tracks input changes to avoid unnecessary
|
||||
re-execution while still ensuring prompts are saved when inputs change.
|
||||
|
||||
Returns:
|
||||
A hash of the input values that changes when any input changes
|
||||
"""
|
||||
# Create a hash of all the text inputs that affect the output
|
||||
# This ensures the node only re-executes when inputs actually change
|
||||
import hashlib
|
||||
|
||||
# Combine all text inputs that affect the output
|
||||
combined = f"{text}|{category}|{tags}|{prepend_text}|{append_text}"
|
||||
|
||||
# Return a hash that will change when inputs change
|
||||
# Note: We don't include search_text as it doesn't affect the output
|
||||
return hashlib.sha256(combined.encode()).hexdigest()
|
||||
# NOTE: IS_CHANGED intentionally removed to match CLIPTextEncode behavior
|
||||
# ComfyUI's default caching (based on input values) should handle cache invalidation
|
||||
# The previous IS_CHANGED implementation was causing input/cache mismatch issues
|
||||
|
||||
@@ -302,6 +302,46 @@ class PromptManagerAPI:
|
||||
status=500,
|
||||
)
|
||||
|
||||
# Serve static files from web/lib directory
|
||||
@routes.get("/prompt_manager/lib/{filepath:.*}")
|
||||
async def serve_lib_static(request):
|
||||
"""Serve static library files (JS, CSS) from web/lib directory."""
|
||||
import os
|
||||
|
||||
# Explicit MIME type mapping
|
||||
MIME_TYPES = {
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".map": "application/json",
|
||||
}
|
||||
|
||||
filepath = request.match_info.get("filepath", "")
|
||||
|
||||
# Security: prevent directory traversal
|
||||
if ".." in filepath or filepath.startswith("/"):
|
||||
return web.Response(text="Forbidden", status=403)
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
file_path = os.path.join(current_dir, "web", "lib", filepath)
|
||||
|
||||
if not os.path.exists(file_path) or not os.path.isfile(file_path):
|
||||
return web.Response(text=f"Not Found: {filepath}", status=404)
|
||||
|
||||
# Get extension and content type
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
content_type = MIME_TYPES.get(ext, "application/octet-stream")
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
return web.Response(
|
||||
body=content,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
# Statistics endpoint
|
||||
@routes.get("/prompt_manager/stats")
|
||||
async def get_stats_route(request):
|
||||
@@ -1021,10 +1061,11 @@ class PromptManagerAPI:
|
||||
thumbnails_dir = output_path / "thumbnails"
|
||||
if thumbnails_dir.exists():
|
||||
thumbnail_ext = '.jpg' if is_video else extension
|
||||
# Use full relative path to support subdirectories
|
||||
thumbnail_rel_path = Path("thumbnails") / rel_path.parent / f"{media_path.stem}_thumb{thumbnail_ext}"
|
||||
# Preserve subdirectory structure in thumbnail path
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
thumbnail_rel_path = f"thumbnails/{rel_path_no_ext.as_posix()}_thumb{thumbnail_ext}"
|
||||
thumbnail_abs_path = output_path / thumbnail_rel_path
|
||||
|
||||
|
||||
if thumbnail_abs_path.exists():
|
||||
from urllib.parse import quote
|
||||
thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel_path.as_posix(), safe="/")}'
|
||||
@@ -1148,9 +1189,9 @@ class PromptManagerAPI:
|
||||
|
||||
# Also try to delete associated thumbnail if it exists
|
||||
try:
|
||||
# Use full relative path to support subdirectories
|
||||
rel_path = file_path.relative_to(output_path)
|
||||
thumbnail_path = output_path / "thumbnails" / rel_path.parent / f"{file_path.stem}_thumb{file_path.suffix}"
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
thumbnail_path = output_path / "thumbnails" / f"{rel_path_no_ext.as_posix()}_thumb{file_path.suffix}"
|
||||
if thumbnail_path.exists():
|
||||
os.remove(thumbnail_path)
|
||||
self.logger.debug(f"Deleted associated thumbnail: {thumbnail_path}")
|
||||
@@ -1912,9 +1953,11 @@ class PromptManagerAPI:
|
||||
# For videos, look for thumbnail with .jpg extension
|
||||
# Use full relative path to support subdirectories
|
||||
thumbnail_ext = '.jpg' if is_video else extension
|
||||
thumbnail_rel_path = Path("thumbnails") / rel_path.parent / f"{media_path.stem}_thumb{thumbnail_ext}"
|
||||
# Preserve subdirectory structure in thumbnail path
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
thumbnail_rel_path = f"thumbnails/{rel_path_no_ext.as_posix()}_thumb{thumbnail_ext}"
|
||||
thumbnail_abs_path = output_path / thumbnail_rel_path
|
||||
|
||||
|
||||
if thumbnail_abs_path.exists():
|
||||
from urllib.parse import quote
|
||||
thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel_path.as_posix(), safe="/")}'
|
||||
@@ -2165,11 +2208,12 @@ class PromptManagerAPI:
|
||||
is_video = any(media_file.name.lower().endswith(ext) for ext in video_extensions)
|
||||
|
||||
# For videos, always save thumbnail as .jpg
|
||||
# Include parent directory structure to avoid collisions with same-named files
|
||||
# Preserve subdirectory structure in thumbnail path
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
if is_video:
|
||||
thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb.jpg"
|
||||
thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg"
|
||||
else:
|
||||
thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb{rel_path.suffix}"
|
||||
thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{rel_path.suffix}"
|
||||
|
||||
# Skip if thumbnail already exists and is newer than original
|
||||
if (thumbnail_path.exists() and
|
||||
@@ -2382,11 +2426,12 @@ class PromptManagerAPI:
|
||||
rel_path = media_file.relative_to(output_path)
|
||||
|
||||
# For videos, always save thumbnail as .jpg
|
||||
# Include parent directory structure to avoid collisions with same-named files
|
||||
# Preserve subdirectory structure in thumbnail path
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
if is_video:
|
||||
thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb.jpg"
|
||||
thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb.jpg"
|
||||
else:
|
||||
thumbnail_path = thumbnails_dir / rel_path.parent / f"{rel_path.stem}_thumb{rel_path.suffix}"
|
||||
thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{rel_path.suffix}"
|
||||
|
||||
# SAFETY: Ensure thumbnail path is within our thumbnails directory
|
||||
try:
|
||||
@@ -3002,7 +3047,27 @@ class PromptManagerAPI:
|
||||
'status': 'ok' if output_dirs else 'warning',
|
||||
'output_dirs': output_dirs
|
||||
}
|
||||
|
||||
|
||||
# Check image monitor status
|
||||
try:
|
||||
from ..utils.image_monitor import _monitor_instance
|
||||
if _monitor_instance is not None:
|
||||
monitor_status = _monitor_instance.get_status()
|
||||
results['image_monitor'] = {
|
||||
'status': 'ok' if monitor_status.get('observer_alive') else 'error',
|
||||
**monitor_status
|
||||
}
|
||||
else:
|
||||
results['image_monitor'] = {
|
||||
'status': 'error',
|
||||
'message': 'Image monitor not initialized'
|
||||
}
|
||||
except Exception as e:
|
||||
results['image_monitor'] = {
|
||||
'status': 'error',
|
||||
'message': f'Failed to get monitor status: {str(e)}'
|
||||
}
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'diagnostics': results
|
||||
@@ -4747,12 +4812,11 @@ class PromptManagerAPI:
|
||||
thumbnail_url = None
|
||||
thumbnails_dir = output_path / "thumbnails"
|
||||
if thumbnails_dir.exists():
|
||||
# Use full relative path to support subdirectories
|
||||
thumbnail_rel = Path("thumbnails") / rel_path.parent / f"{image_path.stem}_thumb{image_path.suffix}"
|
||||
thumbnail_path = output_path / thumbnail_rel
|
||||
# Preserve subdirectory structure in thumbnail path
|
||||
rel_path_no_ext = rel_path.with_suffix('')
|
||||
thumbnail_path = thumbnails_dir / f"{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}"
|
||||
if thumbnail_path.exists():
|
||||
from urllib.parse import quote
|
||||
thumbnail_url = f'/prompt_manager/images/serve/{quote(thumbnail_rel.as_posix(), safe="/")}'
|
||||
thumbnail_url = f'/prompt_manager/images/serve/thumbnails/{rel_path_no_ext.as_posix()}_thumb{image_path.suffix}'
|
||||
|
||||
images.append({
|
||||
'filename': image_path.name,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "promptmanager"
|
||||
description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite."
|
||||
version = "3.0.26"
|
||||
version = "3.0.27"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"]
|
||||
|
||||
|
||||
+8
-8
@@ -30,16 +30,16 @@ sys.path.insert(0, current_dir)
|
||||
try:
|
||||
# Import and initialize the gallery system
|
||||
from database.operations import PromptDatabase
|
||||
from utils.prompt_tracker import PromptTracker
|
||||
from utils.image_monitor import ImageMonitor
|
||||
|
||||
from utils.prompt_tracker import get_prompt_tracker
|
||||
from utils.image_monitor import get_image_monitor
|
||||
|
||||
print("[RESTART] Restarting PromptManager Gallery System...")
|
||||
|
||||
# Initialize components
|
||||
|
||||
# Initialize components using singleton getters
|
||||
db = PromptDatabase()
|
||||
tracker = PromptTracker(db)
|
||||
monitor = ImageMonitor(db, tracker)
|
||||
|
||||
tracker = get_prompt_tracker(db)
|
||||
monitor = get_image_monitor(db, tracker)
|
||||
|
||||
print("[SUCCESS] Components initialized successfully")
|
||||
|
||||
# Test image monitoring directories
|
||||
|
||||
+47
-13
@@ -75,11 +75,11 @@ class ImageGenerationHandler(FileSystemEventHandler):
|
||||
event: FileSystemEvent object containing event details
|
||||
"""
|
||||
if not event.is_directory and self.is_image_file(event.src_path):
|
||||
self.logger.debug(f"New image detected: {event.src_path}")
|
||||
self.logger.info(f"New image detected: {event.src_path}")
|
||||
# Small delay to ensure file is fully written
|
||||
threading.Timer(
|
||||
self.processing_delay,
|
||||
self.process_new_image,
|
||||
self.processing_delay,
|
||||
self.process_new_image,
|
||||
args=[event.src_path]
|
||||
).start()
|
||||
|
||||
@@ -108,15 +108,15 @@ class ImageGenerationHandler(FileSystemEventHandler):
|
||||
image_path: Full path to the newly created image file
|
||||
"""
|
||||
try:
|
||||
self.logger.debug(f"Processing image: {image_path}")
|
||||
|
||||
self.logger.info(f"Processing image: {image_path}")
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
self.logger.warning(f"Image file no longer exists: {image_path}")
|
||||
return
|
||||
|
||||
|
||||
# Get current prompt context first
|
||||
current_prompt = self.prompt_tracker.get_current_prompt()
|
||||
self.logger.debug(f"Current prompt context: {current_prompt['id'] if current_prompt else 'None'}")
|
||||
self.logger.info(f"Current prompt context: {current_prompt['id'] if current_prompt else 'None'}")
|
||||
|
||||
if not current_prompt:
|
||||
self.logger.debug(f"No active prompt context for image: {image_path}")
|
||||
@@ -301,13 +301,13 @@ class ImageMonitor:
|
||||
if os.path.exists(output_dir):
|
||||
self.observer.schedule(self.handler, output_dir, recursive=True)
|
||||
self.monitored_directories.append(output_dir)
|
||||
self.logger.debug(f"Monitoring directory: {output_dir}")
|
||||
self.logger.info(f"Monitoring directory (recursive): {output_dir}")
|
||||
else:
|
||||
self.logger.warning(f"Directory does not exist: {output_dir}")
|
||||
|
||||
|
||||
if self.monitored_directories:
|
||||
self.observer.start()
|
||||
self.logger.debug(f"Image monitoring started for {len(self.monitored_directories)} directories")
|
||||
self.logger.info(f"Image monitoring started for {len(self.monitored_directories)} directories")
|
||||
else:
|
||||
self.logger.warning("No valid directories to monitor")
|
||||
|
||||
@@ -367,15 +367,49 @@ class ImageMonitor:
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get monitoring status information.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- running: Boolean indicating if monitoring is active
|
||||
- monitored_directories: List of currently monitored directory paths
|
||||
- handler_active: Boolean indicating if the event handler is active
|
||||
- observer_alive: Boolean indicating if observer thread is alive
|
||||
"""
|
||||
observer_alive = False
|
||||
if self.observer is not None:
|
||||
try:
|
||||
observer_alive = self.observer.is_alive()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
'running': self.observer is not None,
|
||||
'monitored_directories': self.monitored_directories,
|
||||
'handler_active': self.handler is not None
|
||||
}
|
||||
'handler_active': self.handler is not None,
|
||||
'observer_alive': observer_alive
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance management
|
||||
_monitor_instance: Optional[ImageMonitor] = None
|
||||
_monitor_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_image_monitor(db_manager, prompt_tracker) -> ImageMonitor:
|
||||
"""Get or create the singleton ImageMonitor instance.
|
||||
|
||||
This ensures only one ImageMonitor exists across all PromptManager nodes,
|
||||
preventing duplicate image detection when multiple nodes are used.
|
||||
|
||||
Args:
|
||||
db_manager: Database manager instance for storing image relationships
|
||||
prompt_tracker: Prompt tracking instance for getting active prompt context
|
||||
|
||||
Returns:
|
||||
The singleton ImageMonitor instance
|
||||
"""
|
||||
global _monitor_instance
|
||||
if _monitor_instance is None:
|
||||
with _monitor_lock:
|
||||
if _monitor_instance is None:
|
||||
_monitor_instance = ImageMonitor(db_manager, prompt_tracker)
|
||||
return _monitor_instance
|
||||
+29
-4
@@ -408,16 +408,41 @@ class PromptExecutionContext:
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit the execution context.
|
||||
|
||||
|
||||
Currently does not clear the prompt context to allow images to be
|
||||
generated after prompt execution completes. The prompt will be cleaned
|
||||
up automatically after the timeout period.
|
||||
|
||||
|
||||
Args:
|
||||
exc_type: Exception type (if an exception occurred)
|
||||
exc_val: Exception value (if an exception occurred)
|
||||
exc_val: Exception value (if an exception occurred)
|
||||
exc_tb: Exception traceback (if an exception occurred)
|
||||
"""
|
||||
# Don't clear immediately - let the timeout handle it
|
||||
# This allows images to be generated after the prompt execution completes
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
# Singleton instance management
|
||||
_tracker_instance: Optional[PromptTracker] = None
|
||||
_tracker_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_prompt_tracker(db_manager) -> PromptTracker:
|
||||
"""Get or create the singleton PromptTracker instance.
|
||||
|
||||
This ensures only one PromptTracker exists across all PromptManager nodes,
|
||||
preventing duplicate image linking when multiple nodes are used.
|
||||
|
||||
Args:
|
||||
db_manager: Database manager instance for prompt operations
|
||||
|
||||
Returns:
|
||||
The singleton PromptTracker instance
|
||||
"""
|
||||
global _tracker_instance
|
||||
if _tracker_instance is None:
|
||||
with _tracker_lock:
|
||||
if _tracker_instance is None:
|
||||
_tracker_instance = PromptTracker(db_manager)
|
||||
return _tracker_instance
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PromptManager - Admin Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.6/viewer.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.6/viewer.min.js"></script>
|
||||
<script src="lib/tailwind/tailwind.js"></script>
|
||||
<link rel="stylesheet" href="lib/viewerjs/viewer.min.css">
|
||||
<script src="lib/viewerjs/viewer.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PromptManager - Gallery</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.6/viewer.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.6/viewer.min.js"></script>
|
||||
<script src="lib/tailwind/tailwind.js"></script>
|
||||
<link rel="stylesheet" href="lib/viewerjs/viewer.min.css">
|
||||
<script src="lib/viewerjs/viewer.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Image Gallery - Generation Data</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="lib/tailwind/tailwind.js"></script>
|
||||
<style>
|
||||
/* Ensure the layout takes full height and prevents overflow */
|
||||
html, body {
|
||||
|
||||
+237
-5
@@ -1,6 +1,7 @@
|
||||
// PromptManager/web/prompt_manager.js
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
// ============================================================================
|
||||
// Top Bar Button Integration (similar to rgthree-comfy approach)
|
||||
@@ -158,6 +159,47 @@ function addPMTopBarButtonLegacy() {
|
||||
// Extension Registration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* CRITICAL FIX: Sync all PromptManager widget values from DOM
|
||||
* This must be called before any serialization happens
|
||||
*/
|
||||
function syncPromptManagerWidgets() {
|
||||
const graph = app.graph;
|
||||
if (!graph) return;
|
||||
|
||||
const nodes = graph._nodes || graph.nodes || [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === "PromptManager" || node.type === "PromptManagerText") {
|
||||
if (node.widgets && node.widgets.length > 0) {
|
||||
if (!node.widgets_values) {
|
||||
node.widgets_values = [];
|
||||
}
|
||||
|
||||
node.widgets.forEach((widget, idx) => {
|
||||
if (widget) {
|
||||
// CRITICAL: For multiline widgets, inputEl.value is the true source
|
||||
const actualValue = widget.inputEl ? widget.inputEl.value : widget.value;
|
||||
if (actualValue !== undefined) {
|
||||
const oldValue = node.widgets_values[idx];
|
||||
node.widgets_values[idx] = actualValue;
|
||||
|
||||
// Also sync widget.value
|
||||
if (widget.inputEl && widget.value !== actualValue) {
|
||||
widget.value = actualValue;
|
||||
}
|
||||
|
||||
if (widget.name === "text" && oldValue !== actualValue) {
|
||||
console.log(`[PromptManager] graphToPrompt sync: "${String(oldValue).substring(0, 30)}..." -> "${String(actualValue).substring(0, 30)}..."`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "PromptManager.UI",
|
||||
|
||||
@@ -166,6 +208,56 @@ app.registerExtension({
|
||||
setTimeout(() => {
|
||||
addPMTopBarButton();
|
||||
}, 100);
|
||||
|
||||
// CRITICAL: Wrap app.graphToPrompt to sync widget values BEFORE serialization
|
||||
// This runs before any other extension's wrapper can access stale data
|
||||
const originalGraphToPrompt = app.graphToPrompt.bind(app);
|
||||
app.graphToPrompt = async function(...args) {
|
||||
console.log("[PromptManager] graphToPrompt intercepted - syncing widgets NOW");
|
||||
syncPromptManagerWidgets();
|
||||
return originalGraphToPrompt(...args);
|
||||
};
|
||||
console.log("[PromptManager] Wrapped app.graphToPrompt for value sync");
|
||||
|
||||
// CRITICAL FIX: Intercept api.queuePrompt to fix prompt data at the LAST moment
|
||||
// This catches any cached/stale data that other extensions might have passed through
|
||||
const originalQueuePrompt = api.queuePrompt.bind(api);
|
||||
api.queuePrompt = async function(number, { output, workflow }) {
|
||||
console.log("[PromptManager] api.queuePrompt intercepted - fixing prompt data");
|
||||
|
||||
// Find PromptManager nodes in the output and fix their text values
|
||||
const graph = app.graph;
|
||||
if (graph && output) {
|
||||
const nodes = graph._nodes || graph.nodes || [];
|
||||
for (const node of nodes) {
|
||||
if ((node.type === "PromptManager" || node.type === "PromptManagerText") && output[node.id]) {
|
||||
// Find the text widget and get its current DOM value
|
||||
const textWidget = node.widgets?.find(w => w.name === "text");
|
||||
if (textWidget && textWidget.inputEl) {
|
||||
const currentValue = textWidget.inputEl.value;
|
||||
const outputNode = output[node.id];
|
||||
|
||||
// The output structure has "inputs" with widget values
|
||||
if (outputNode.inputs && outputNode.inputs.text !== currentValue) {
|
||||
console.log(`[PromptManager] FIXING node ${node.id} text: "${String(outputNode.inputs.text).substring(0, 30)}..." -> "${currentValue.substring(0, 30)}..."`);
|
||||
outputNode.inputs.text = currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return originalQueuePrompt(number, { output, workflow });
|
||||
};
|
||||
console.log("[PromptManager] Wrapped api.queuePrompt for final data fix");
|
||||
},
|
||||
|
||||
/**
|
||||
* CRITICAL: Also hook beforeQueuePrompt as a backup sync point
|
||||
*/
|
||||
async beforeQueuePrompt(promptInfo) {
|
||||
console.log("[PromptManager] beforeQueuePrompt - additional sync");
|
||||
syncPromptManagerWidgets();
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
@@ -181,6 +273,62 @@ app.registerExtension({
|
||||
onNodeCreated.apply(this, arguments);
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Ensure text widget always returns current value during serialization
|
||||
// This fixes the issue where graphToPrompt reads stale cached values
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
const textWidget = self.widgets?.find((w) => w.name === "text");
|
||||
if (textWidget) {
|
||||
console.log("[PromptManager] Patching text widget for proper serialization");
|
||||
|
||||
// Store reference to the widget for the closure
|
||||
const widgetRef = textWidget;
|
||||
const originalSerializeValue = textWidget.serializeValue;
|
||||
|
||||
// Override serializeValue - use arrow function to capture widgetRef
|
||||
textWidget.serializeValue = (nodeId, widgetIndex) => {
|
||||
// Always return the current DOM value using our stored reference
|
||||
if (widgetRef.inputEl) {
|
||||
const currentValue = widgetRef.inputEl.value;
|
||||
// Sync widget.value as well
|
||||
widgetRef.value = currentValue;
|
||||
console.log(`[PromptManager] serializeValue returning: "${currentValue.substring(0, 50)}..."`);
|
||||
return currentValue;
|
||||
}
|
||||
// Fallback to widget.value
|
||||
console.log(`[PromptManager] serializeValue fallback to widget.value: "${String(widgetRef.value).substring(0, 50)}..."`);
|
||||
return widgetRef.value;
|
||||
};
|
||||
|
||||
// Add aggressive sync listeners for all value change events
|
||||
if (textWidget.inputEl) {
|
||||
const syncValue = function() {
|
||||
const currentVal = textWidget.inputEl.value;
|
||||
textWidget.value = currentVal;
|
||||
// Also update widgets_values array
|
||||
const widgetIndex = self.widgets?.indexOf(textWidget);
|
||||
if (self.widgets_values && widgetIndex >= 0) {
|
||||
self.widgets_values[widgetIndex] = currentVal;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync on every keystroke
|
||||
textWidget.inputEl.addEventListener('input', syncValue);
|
||||
// Sync on change (for paste, autocomplete, etc.)
|
||||
textWidget.inputEl.addEventListener('change', syncValue);
|
||||
// Sync when focus leaves the field
|
||||
textWidget.inputEl.addEventListener('blur', syncValue);
|
||||
// Sync on keyup as backup
|
||||
textWidget.inputEl.addEventListener('keyup', syncValue);
|
||||
|
||||
// Initial sync
|
||||
syncValue();
|
||||
|
||||
console.log("[PromptManager] Added comprehensive input listeners to text widget");
|
||||
}
|
||||
}
|
||||
}, 50); // Shorter delay to beat other extensions
|
||||
|
||||
// Initialize properties for search state (non-serialized runtime state)
|
||||
this.properties = this.properties || {};
|
||||
this.properties.resultTimeout = 3; // Default 3 seconds
|
||||
@@ -261,12 +409,40 @@ app.registerExtension({
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into serialization to preserve resize flag and prevent runtime data from being saved
|
||||
// Hook into serialization to preserve resize flag, sync widget values, and prevent runtime data from being saved
|
||||
const originalSerialize = this.serialize;
|
||||
this.serialize = function () {
|
||||
// CRITICAL: Sync widget values BEFORE serialization
|
||||
// This ensures graphToPrompt reads current values, not cached ones
|
||||
// For multiline widgets, the value is stored in inputEl.value
|
||||
if (this.widgets && this.widgets.length > 0) {
|
||||
if (!this.widgets_values) {
|
||||
this.widgets_values = [];
|
||||
}
|
||||
this.widgets.forEach((widget, idx) => {
|
||||
if (widget) {
|
||||
// Get the actual value - inputEl.value for DOM widgets, otherwise widget.value
|
||||
const actualValue = widget.inputEl ? widget.inputEl.value : widget.value;
|
||||
if (actualValue !== undefined) {
|
||||
this.widgets_values[idx] = actualValue;
|
||||
// Sync widget.value with inputEl.value
|
||||
if (widget.inputEl && widget.value !== actualValue) {
|
||||
widget.value = actualValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`[PromptManager] serialize: Synced ${this.widgets.length} widget values`);
|
||||
}
|
||||
|
||||
const data = originalSerialize ? originalSerialize.call(this) : {};
|
||||
data._userHasResized = this._userHasResized;
|
||||
|
||||
|
||||
// Ensure widgets_values is included in serialization
|
||||
if (this.widgets_values) {
|
||||
data.widgets_values = [...this.widgets_values];
|
||||
}
|
||||
|
||||
// Ensure search results are never saved to workflow
|
||||
if (data.properties && data.properties.searchResults) {
|
||||
delete data.properties.searchResults;
|
||||
@@ -274,7 +450,7 @@ app.registerExtension({
|
||||
if (data.properties && data.properties.selectedPromptIndex) {
|
||||
delete data.properties.selectedPromptIndex;
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -677,32 +853,88 @@ app.registerExtension({
|
||||
// Find the text widget and set its value
|
||||
const textWidget = this.widgets?.find((w) => w.name === "text");
|
||||
if (textWidget) {
|
||||
const textWidgetIndex = this.widgets.indexOf(textWidget);
|
||||
|
||||
console.log(`[PromptManager] DEBUG: Setting prompt on node ${this.id}`);
|
||||
console.log(`[PromptManager] DEBUG: textWidget found at index ${textWidgetIndex}`);
|
||||
console.log(`[PromptManager] DEBUG: Old value: ${textWidget.value?.substring(0, 50)}...`);
|
||||
console.log(`[PromptManager] DEBUG: New value: ${prompt.text.substring(0, 50)}...`);
|
||||
|
||||
// Set the widget value
|
||||
textWidget.value = prompt.text;
|
||||
|
||||
// CRITICAL: For multiline widgets, the value is stored in the DOM element (inputEl)
|
||||
// graphToPrompt calls serializeValue() which reads from inputEl.value
|
||||
if (textWidget.inputEl) {
|
||||
textWidget.inputEl.value = prompt.text;
|
||||
console.log(`[PromptManager] DEBUG: Updated inputEl.value directly`);
|
||||
}
|
||||
|
||||
// CRITICAL: Also update widgets_values array if it exists
|
||||
// ComfyUI's graphToPrompt reads from this array for execution
|
||||
if (this.widgets_values && textWidgetIndex >= 0) {
|
||||
this.widgets_values[textWidgetIndex] = prompt.text;
|
||||
console.log(`[PromptManager] DEBUG: Updated widgets_values[${textWidgetIndex}]`);
|
||||
}
|
||||
|
||||
// Trigger widget callback to notify ComfyUI of value change
|
||||
if (textWidget.callback) {
|
||||
textWidget.callback(prompt.text, app.canvas, this, null, null);
|
||||
console.log(`[PromptManager] DEBUG: Called textWidget.callback`);
|
||||
}
|
||||
|
||||
// Also set metadata if available
|
||||
const categoryWidget = this.widgets?.find(
|
||||
(w) => w.name === "category",
|
||||
);
|
||||
if (categoryWidget && prompt.category) {
|
||||
const categoryIdx = this.widgets.indexOf(categoryWidget);
|
||||
categoryWidget.value = prompt.category;
|
||||
if (categoryWidget.inputEl) {
|
||||
categoryWidget.inputEl.value = prompt.category;
|
||||
}
|
||||
if (this.widgets_values && categoryIdx >= 0) {
|
||||
this.widgets_values[categoryIdx] = prompt.category;
|
||||
}
|
||||
if (categoryWidget.callback) {
|
||||
categoryWidget.callback(prompt.category, app.canvas, this, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
const tagsWidget = this.widgets?.find((w) => w.name === "tags");
|
||||
if (tagsWidget && prompt.tags) {
|
||||
tagsWidget.value = Array.isArray(prompt.tags)
|
||||
const tagsIdx = this.widgets.indexOf(tagsWidget);
|
||||
const tagsValue = Array.isArray(prompt.tags)
|
||||
? prompt.tags.join(", ")
|
||||
: prompt.tags;
|
||||
tagsWidget.value = tagsValue;
|
||||
if (tagsWidget.inputEl) {
|
||||
tagsWidget.inputEl.value = tagsValue;
|
||||
}
|
||||
if (this.widgets_values && tagsIdx >= 0) {
|
||||
this.widgets_values[tagsIdx] = tagsValue;
|
||||
}
|
||||
if (tagsWidget.callback) {
|
||||
tagsWidget.callback(tagsValue, app.canvas, this, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Visual feedback
|
||||
this.highlightSelectedResult(index);
|
||||
|
||||
// Trigger widget change events
|
||||
// Trigger widget change events and mark graph as needing re-execution
|
||||
this.setDirtyCanvas(true, true);
|
||||
|
||||
// Force graph to recognize value change for execution cache invalidation
|
||||
if (app.graph) {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[PromptManager] Loaded prompt: ${prompt.text.substring(0, 50)}...`,
|
||||
);
|
||||
console.log(`[PromptManager] DEBUG: Final widget value: ${textWidget.value?.substring(0, 50)}...`);
|
||||
console.log(`[PromptManager] DEBUG: widgets_values: ${JSON.stringify(this.widgets_values?.slice(0, 3))}...`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user