Merge pull request #30 from ComfyAssets/text-only

Text only
This commit is contained in:
Vito
2025-06-08 15:52:55 -07:00
committed by GitHub
6 changed files with 492 additions and 30 deletions
+116 -24
View File
@@ -6,9 +6,27 @@ A comprehensive ComfyUI custom node that extends the standard text encoder with
![Prompt Manager Interface](images/pm-01.png)
ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard `CLIPTextEncode` node while adding comprehensive prompt management and automatic image tracking features:
ComfyUI Prompt Manager provides two powerful node types for comprehensive prompt management:
### **PromptManager** (CLIP Encoding Node)
A drop-in replacement for ComfyUI's standard `CLIPTextEncode` node with database features:
- **🔄 Drop-in Replacement**: Works exactly like the standard text encoder
- **🤖 CLIP Encoding**: Outputs CONDITIONING for use with samplers and other CLIP-dependent nodes
### **PromptManagerText** (Text-Only Node)
A text-focused variant that outputs raw STRING for maximum flexibility:
- **📝 Pure Text Output**: Outputs STRING type for use with any text-accepting node
- **🔗 Text Concatenation**: Built-in prepend/append functionality for dynamic text assembly
- **⚡ Lightweight Processing**: No CLIP encoding overhead for text-only workflows
### **Shared Features**
Both nodes include the complete PromptManager feature set:
- **💾 Persistent Storage**: Automatically saves all prompts to a local SQLite database
- **🔍 Advanced Search**: Query past prompts with text search, category filtering, and metadata
- **🖼️ Automatic Image Gallery**: Automatically links generated images to their prompts
@@ -26,7 +44,21 @@ ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard
### Core Functionality
#### PromptManager (CLIP Encoding)
- **Text Encoding**: Standard CLIP text encoding for ComfyUI workflows
- **Conditioning Output**: Produces CONDITIONING for samplers and generation nodes
- **Full Compatibility**: Drop-in replacement for CLIPTextEncode nodes
#### PromptManagerText (Text-Only)
- **String Output**: Produces STRING for use with any text-accepting node
- **Text Concatenation**: Prepend and append fields for dynamic text assembly
- **Lightweight**: No CLIP encoding overhead for pure text workflows
- **Flexible Integration**: Works with custom nodes, text processors, and other STRING inputs
#### Shared Features
- **Auto-Save**: Every prompt is automatically saved to the database
- **Metadata Support**: Optional categories, tags, ratings (1-5), notes, and workflow names
- **Hash-based Deduplication**: Prevents storing identical prompts multiple times
@@ -51,7 +83,7 @@ The image gallery has been completely redesigned with professional image viewing
- **File Path Integration**: Direct access to image files in your output directory
- **Responsive Design**: Works seamlessly across desktop, tablet, and mobile devices
*Image viewing powered by [ViewerJS](https://github.com/fengyuanchen/viewerjs) - A powerful JavaScript image viewer with zoom, rotation, and slideshow features.*
_Image viewing powered by [ViewerJS](https://github.com/fengyuanchen/viewerjs) - A powerful JavaScript image viewer with zoom, rotation, and slideshow features._
### 🔬 ComfyUI Workflow Analysis
@@ -133,10 +165,10 @@ The image gallery has been completely redesigned with professional image viewing
3. **Restart ComfyUI** to load the new node
4. **Add the node** to your workflow:
4. **Add the nodes** to your workflow:
- Look for "Prompt Manager" in the conditioning category
- Use it exactly like the standard "CLIP Text Encode" node
- **PromptManager**: Look for "Prompt Manager" in the conditioning category - use exactly like "CLIP Text Encode"
- **PromptManagerText**: Look for "Prompt Manager Text" in the PromptManager/Text category - outputs STRING for any text node
5. **Access the web interface**:
- **Admin Dashboard**: `http://localhost:8188/prompt_manager/admin` - Full management interface
@@ -154,19 +186,37 @@ The image gallery has been completely redesigned with professional image viewing
### Basic Usage
#### Using PromptManager (CLIP Encoding)
Replace any `CLIPTextEncode` node with `PromptManager`:
1. **Add the node** to your workflow
1. **Add the PromptManager node** to your workflow
2. **Connect CLIP model** (same as standard text encoder)
3. **Enter your prompt** in the text field
4. **Optionally add metadata**:
- Category: "portraits", "landscapes", "abstract", etc.
- Tags: "detailed, anime, masterpiece" (comma-separated)
- Rating: 1-5 stars for prompt quality
- Notes: Any additional information
- Workflow Name: Name of your workflow for organization
4. **Connect the CONDITIONING output** to your sampler node
The node will encode your text, automatically save it to the database, and link any generated images to the prompt.
#### Using PromptManagerText (Text-Only)
For text-only workflows or when you need STRING output:
1. **Add the PromptManagerText node** to your workflow
2. **Enter your prompt** in the text field
3. **Optionally use prepend/append** for dynamic text assembly:
- **Prepend Text**: Text added before your main prompt
- **Append Text**: Text added after your main prompt
4. **Connect the STRING output** to any node that accepts text input
#### Shared Metadata Options
Both nodes support the same metadata fields:
- **Category**: "portraits", "landscapes", "abstract", etc.
- **Tags**: "detailed, anime, masterpiece" (comma-separated)
- **Rating**: 1-5 stars for prompt quality (web interface only)
- **Notes**: Any additional information (web interface only)
- **Workflow Name**: Name of your workflow for organization (web interface only)
Both nodes will automatically save prompts to the database and link any generated images to the prompt.
### 🖼️ Using the Image Gallery
@@ -274,34 +324,63 @@ images = db.get_prompt_images(prompt_id="123")
## Examples
### Example 1: Basic Prompt Storage
### PromptManager (CLIP Encoding) Examples
#### Example 1: Basic Prompt Storage
```
Input: "A beautiful sunset over a mountain lake"
Category: "landscapes"
Tags: "nature, sunset, mountains, water"
Rating: 5
Notes: "Perfect for peaceful scenes"
→ Outputs: CONDITIONING for sampler nodes
```
### Example 2: Character Prompt
#### Example 2: Character Prompt
```
Input: "Portrait of a cyberpunk hacker with neon implants"
Category: "characters"
Tags: "cyberpunk, portrait, sci-fi, neon"
Rating: 4
Workflow: "character_generator_v2"
→ Outputs: CONDITIONING for sampler nodes
```
### Example 3: Abstract Art
### PromptManagerText (Text-Only) Examples
#### Example 3: Dynamic Text Assembly
```
Main Text: "beautiful landscape"
Prepend Text: "masterpiece, ultra detailed,"
Append Text: ", 8k resolution, trending on artstation"
→ Final Output: "masterpiece, ultra detailed, beautiful landscape, 8k resolution, trending on artstation"
```
#### Example 4: Text Processing Pipeline
```
Input: "A serene mountain lake at sunset"
Category: "landscapes"
Tags: "nature, peaceful, golden hour"
→ Outputs: STRING for use with text processors, style nodes, or other custom nodes
```
#### Example 5: Multi-Node Text Workflow
```
PromptManagerText → Text Processor → Style Applicator → Final Text Node
"cyberpunk city" → style processing → "neon-lit cyberpunk metropolis at night"
```
### Traditional Examples
#### Example 6: Abstract Art
```
Input: "Swirling colors in an abstract geometric pattern"
Category: "abstract"
Tags: "geometric, colorful, pattern, modern"
Rating: 3
Notes: "Good for experimental art"
Rating: 3 (set via web interface)
Notes: "Good for experimental art" (set via web interface)
```
## Database Schema
@@ -343,7 +422,8 @@ CREATE TABLE generated_images (
### Core Components
- **`prompt_manager.py`** - Main ComfyUI node implementation
- **`prompt_manager.py`** - Main ComfyUI node implementation (CLIP encoding)
- **`prompt_manager_text.py`** - Text-only node implementation (STRING output)
- **`database/models.py`** - Database schema and connection management
- **`database/operations.py`** - CRUD operations and search functionality
- **`py/api.py`** - Web API endpoints for the interface
@@ -365,7 +445,8 @@ CREATE TABLE generated_images (
```
ComfyUI_PromptManager/
├── __init__.py # Node registration
├── prompt_manager.py # Main node implementation
├── prompt_manager.py # Main node implementation (CLIP encoding)
├── prompt_manager_text.py # Text-only node implementation (STRING output)
├── database/
│ ├── __init__.py
│ ├── models.py # Database schema
@@ -575,6 +656,17 @@ MIT License - see LICENSE file for details.
## Changelog
### v3.0.5 (Text-Only Node Release)
- **📝 PromptManagerText Node**: New text-only variant that outputs STRING instead of CONDITIONING
- **🔗 Text Concatenation**: Built-in prepend/append functionality for dynamic text assembly
- **⚡ Lightweight Processing**: No CLIP encoding overhead for pure text workflows
- **🔄 Dual Node Architecture**: Choose between CLIP encoding (PromptManager) or text-only (PromptManagerText)
- **🏷️ Shared Metadata System**: Both nodes use the same database and metadata features
- **🖼️ Unified Gallery**: Images from both node types appear in the same gallery system
- **📊 Enhanced Tracking**: Improved prompt tracking with prepend/append context preservation
- **📊 Enhanced Tracking**: Improved prompt tracking with prepend/append context preservation
### v3.0.3 (ViewerJS Integration & Enhanced Gallery)
- **🖼️ Professional Image Viewer**: Complete gallery redesign with [ViewerJS](https://github.com/fengyuanchen/viewerjs) integration
+11
View File
@@ -29,12 +29,23 @@ except Exception as e:
log_message(f"Traceback: {traceback.format_exc()}", 'error')
raise
try:
from .prompt_manager_text import PromptManagerText
log_message("Successfully imported PromptManagerText class")
except Exception as e:
log_message(f"ERROR: Failed to import PromptManagerText class: {e}", 'error')
import traceback
log_message(f"Traceback: {traceback.format_exc()}", 'error')
raise
NODE_CLASS_MAPPINGS = {
"PromptManager": PromptManager,
"PromptManagerText": PromptManagerText,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptManager": "Prompt Manager",
"PromptManagerText": "Prompt Manager Text",
}
log_message(f"NODE_CLASS_MAPPINGS: {NODE_CLASS_MAPPINGS}")
+4 -3
View File
@@ -102,9 +102,10 @@ class PromptManager(ComfyNodeABC):
}
}
RETURN_TYPES = (IO.CONDITIONING,)
RETURN_TYPES = (IO.CONDITIONING, IO.STRING)
OUTPUT_TOOLTIPS = (
"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"
CATEGORY = "PromptManager/Text"
@@ -137,7 +138,7 @@ class PromptManager(ComfyNodeABC):
append_text: Text to append to the main prompt
Returns:
Tuple containing the conditioning for the diffusion model
Tuple containing the conditioning for the diffusion model and the final text string
Raises:
RuntimeError: If clip input is invalid
@@ -213,7 +214,7 @@ class PromptManager(ComfyNodeABC):
conditioning = clip.encode_from_tokens_scheduled(tokens)
self.logger.debug("CLIP encoding completed successfully")
return (conditioning,)
return (conditioning, encoding_text)
def _save_prompt_to_database(
self,
+355
View File
@@ -0,0 +1,355 @@
"""
PromptManagerText: A text-only version of PromptManager that outputs STRING
without CLIP encoding, while maintaining all database and search features.
"""
import hashlib
import datetime
import json
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:
# Fallback for older ComfyUI versions
class ComfyNodeABC:
pass
class IO:
STRING = "STRING"
CLIP = "CLIP"
CONDITIONING = "CONDITIONING"
InputTypeDict = dict
try:
from .database.operations import PromptDatabase
from .utils.prompt_tracker import PromptTracker, PromptExecutionContext
from .utils.image_monitor import ImageMonitor
except ImportError:
# For direct imports when not in a package
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from database.operations import PromptDatabase
from utils.prompt_tracker import PromptTracker, PromptExecutionContext
from utils.image_monitor import ImageMonitor
class PromptManagerText(ComfyNodeABC):
"""
A ComfyUI custom node that provides all PromptManager features but outputs
only a STRING without CLIP encoding. Includes:
- Persistent storage of all prompts in SQLite database
- Search and retrieval capabilities
- Metadata management (categories, tags, ratings, notes)
- Duplicate detection via SHA256 hashing
- Text concatenation with prepend/append functionality
"""
def __init__(self):
self.logger = get_logger('prompt_manager_text.node')
self.logger.info("Initializing PromptManagerText 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("PromptManagerText node initialization completed")
@classmethod
def INPUT_TYPES(cls) -> InputTypeDict:
return {
"required": {
"text": (IO.STRING, {
"multiline": True,
"dynamicPrompts": True,
"tooltip": "The text prompt to be processed and saved to database."
})
},
"optional": {
"category": (IO.STRING, {
"default": "",
"tooltip": "Optional category for organizing prompts (e.g., 'landscapes', 'portraits')"
}),
"tags": (IO.STRING, {
"default": "",
"tooltip": "Comma-separated tags for the prompt (e.g., 'anime, detailed, sunset')"
}),
"search_text": (IO.STRING, {
"default": "",
"tooltip": "Search for past prompts containing this text"
}),
"prepend_text": (IO.STRING, {
"default": "",
"tooltip": "Text to prepend to the main prompt (connected STRING nodes will be added before the main text)"
}),
"append_text": (IO.STRING, {
"default": "",
"tooltip": "Text to append to the main prompt (connected STRING nodes will be added after the main text)"
})
}
}
RETURN_TYPES = (IO.STRING,)
OUTPUT_TOOLTIPS = (
"The final combined text string (with prepend/append applied) ready for use in other nodes.",
)
FUNCTION = "process_text"
CATEGORY = "PromptManager/Text"
DESCRIPTION = (
"Processes and manages text prompts with database storage and search capabilities. "
"Outputs a plain STRING that can be used with any node that accepts text input. "
"Includes all PromptManager features: categorization, tagging, search, and prepend/append functionality."
)
def process_text(
self,
text: str,
category: str = "",
tags: str = "",
search_text: str = "",
prepend_text: str = "",
append_text: str = ""
) -> Tuple[str]:
"""
Process the text prompt and save it to the database.
Args:
text: The text prompt to process
category: Optional category for organization
tags: Comma-separated tags
search_text: Text to search for in past prompts
prepend_text: Text to prepend to the main prompt
append_text: Text to append to the main prompt
Returns:
Tuple containing the final processed text string
"""
# Combine prepend, main text, and append text
final_text = ""
if prepend_text and prepend_text.strip():
final_text += prepend_text.strip() + " "
final_text += text
if append_text and append_text.strip():
final_text += " " + append_text.strip()
# For database storage, save the original main text with metadata about prepend/append
storage_text = text
# Search functionality is now handled by the JavaScript UI
# The search parameters are still available for backend processing if needed
# Save prompt to database and set execution context for gallery tracking
prompt_id = None
if storage_text and storage_text.strip():
self.logger.debug(f"Processing prompt text: {storage_text[:100]}...")
# Add prepend/append info to tags if they exist
extended_tags = self._parse_tags(tags) or []
if prepend_text and prepend_text.strip():
extended_tags.append(f"prepend:{prepend_text.strip()[:50]}")
if append_text and append_text.strip():
extended_tags.append(f"append:{append_text.strip()[:50]}")
try:
prompt_id = self._save_prompt_to_database(
text=storage_text.strip(), # Always strip whitespace
category=category.strip() if category else None,
tags=extended_tags if extended_tags else None
)
# Set current prompt for image tracking
if prompt_id:
execution_id = self.prompt_tracker.set_current_prompt(
prompt_text=final_text.strip(), # Use final combined text for tracking
additional_data={
'category': category.strip() if category else None,
'tags': extended_tags,
'prompt_id': prompt_id,
'prepend_text': prepend_text.strip() if prepend_text else None,
'append_text': append_text.strip() if append_text else None
}
)
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 processing
self.logger.warning(f"Failed to save prompt to database: {e}")
self.logger.debug(f"Text processing completed: {final_text[:100]}...")
return (final_text,)
def _save_prompt_to_database(
self,
text: str,
category: Optional[str] = None,
tags: Optional[list] = None
) -> Optional[int]:
"""
Save the prompt to the SQLite database.
Args:
text: The prompt text
category: Optional category
tags: List of tags
Returns:
The prompt ID if saved successfully, None otherwise
"""
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(
prompt_id=existing['id'],
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,
tags=tags,
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:
self.logger.error(f"Error saving prompt to database: {e}")
return None
def _generate_hash(self, text: str) -> str:
"""Generate SHA256 hash for the prompt text."""
# Normalize text for consistent hashing (strip whitespace, normalize case)
normalized_text = text.strip().lower()
return hashlib.sha256(normalized_text.encode('utf-8')).hexdigest()
def _parse_tags(self, tags_string: str) -> Optional[list]:
"""Parse comma-separated tags string into a list."""
if not tags_string or not tags_string.strip():
return None
tags = [tag.strip() for tag in tags_string.split(',') if tag.strip()]
return tags if tags else None
def _search_prompts(self, search_text: str = "") -> List[Dict[str, Any]]:
"""Search for past prompts by text content."""
try:
if not search_text or not search_text.strip():
return []
results = self.db.search_prompts(
text=search_text.strip(),
category=None,
tags=None,
rating_min=None,
limit=50
)
return results
except Exception as e:
self.logger.error(f"Error searching prompts: {e}")
return []
def _open_web_interface(self):
"""Open the web interface in the default browser."""
try:
# Look for a web interface directory
current_dir = os.path.dirname(os.path.abspath(__file__))
web_dir = os.path.join(current_dir, "web_interface")
if os.path.exists(web_dir):
# If web interface exists, try to start it
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. 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)
def get_recent_prompts_api(self, limit: int = 20) -> List[Dict[str, Any]]:
"""API method for JavaScript UI to get recent prompts."""
try:
return self.db.get_recent_prompts(limit=limit)
except Exception as e:
self.logger.error(f"Error getting recent prompts: {e}")
return []
def _start_gallery_system(self):
"""Initialize and start the gallery monitoring system."""
try:
self.logger.info("Starting gallery system...")
# Start image monitoring
self.image_monitor.start_monitoring()
self.logger.info("Gallery system started successfully")
except Exception as e:
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."""
return {
'image_monitor': self.image_monitor.get_status(),
'prompt_tracker': self.prompt_tracker.get_status()
}
def cleanup_gallery_system(self):
"""Clean up gallery system resources."""
try:
if hasattr(self, 'image_monitor'):
self.image_monitor.stop_monitoring()
self.logger.info("Gallery system cleaned up")
except Exception as e:
self.logger.error(f"Error cleaning up gallery system: {e}")
def __del__(self):
"""Cleanup when object is destroyed."""
self.cleanup_gallery_system()
@classmethod
def IS_CHANGED(cls, **kwargs):
"""Always process to ensure database saving."""
return float("NaN") # Always execute
+1 -1
View File
@@ -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.6"
version = "3.0.7"
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"]
+5 -2
View File
@@ -6,8 +6,8 @@ app.registerExtension({
name: "PromptManager.UI",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "PromptManager") {
console.log("[PromptManager] Patching node type for custom UI");
if (nodeData.name === "PromptManager" || nodeData.name === "PromptManagerText") {
console.log(`[PromptManager] Patching node type for custom UI: ${nodeData.name}`);
// Store original methods
const onNodeCreated = nodeType.prototype.onNodeCreated;
@@ -28,6 +28,9 @@ app.registerExtension({
this._searchResults = [];
this._selectedPromptIndex = -1;
this.resultHideTimer = null;
// Store node type for conditional behavior
this.nodeTypeName = nodeData.name;
// Load settings from API
this.loadSettings();