Merge pull request #22 from ComfyAssets/gallery-metadata
Gallery metadata
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ComfyUI Prompt Manager
|
||||
|
||||
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.
|
||||
A comprehensive ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, automatic image gallery system, and powerful ComfyUI workflow metadata analysis using SQLite.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -14,8 +14,11 @@ ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard
|
||||
- **🖼️ Automatic Image Gallery**: Automatically links generated images to their prompts
|
||||
- **🏷️ Rich Metadata**: Add categories, tags, ratings, notes, and workflow names to prompts
|
||||
- **🚫 Duplicate Prevention**: Uses SHA256 hashing to detect and prevent duplicate storage
|
||||
- **🌐 Web Interface**: Beautiful browser-based management interface
|
||||
- **🌐 Advanced Web Interface**: Comprehensive admin dashboard with metadata analysis
|
||||
- **📊 Analytics**: Track prompt usage patterns and effectiveness over time
|
||||
- **🔬 Workflow Analysis**: Extract and analyze ComfyUI workflow data from PNG images
|
||||
- **📋 Metadata Viewer**: Standalone tool for analyzing ComfyUI-generated images
|
||||
- **🛠️ System Management**: Built-in diagnostics, backup/restore, and maintenance tools
|
||||
|
||||

|
||||
|
||||
@@ -39,6 +42,29 @@ ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard
|
||||
- **Full-size Image Viewer**: Modal overlay with navigation controls and keyboard shortcuts
|
||||
- **Image Navigation**: Browse through images with arrow keys or navigation buttons
|
||||
- **Image Counter**: Shows current position in gallery (e.g., "3 / 7")
|
||||
- **Integrated Metadata Panel**: View workflow data directly in the image viewer
|
||||
- **File Path Display**: Shows actual file paths in output directory for easy access
|
||||
|
||||
### 🔬 ComfyUI Workflow Analysis
|
||||
|
||||
- **PNG Metadata Parsing**: Advanced PNG chunk analysis to extract embedded ComfyUI data
|
||||
- **Workflow Data Extraction**: Complete ComfyUI workflow JSON recovery from images
|
||||
- **Prompt Recovery**: Extract positive/negative prompts from any ComfyUI-generated image
|
||||
- **Generation Parameters**: Recover steps, CFG scale, sampler, seed, and model information
|
||||
- **Multi-Node Support**: Handles PromptManager, CLIPTextEncode, and other node types
|
||||
- **Copy & Export**: One-click copying of prompts and workflow JSON download
|
||||
- **Standalone Viewer**: Dedicated metadata analysis tool at `/prompt_manager/gallery.html`
|
||||
- **Drag & Drop Interface**: Easy analysis by dropping PNG files directly into the viewer
|
||||
- **Error Handling**: Robust parsing with NaN value cleaning and malformed JSON recovery
|
||||
|
||||
### 📋 Standalone Metadata Viewer
|
||||
|
||||
- **Independent Tool**: Access at `http://localhost:8188/prompt_manager/gallery.html`
|
||||
- **Real-time Analysis**: Instant PNG metadata extraction and display
|
||||
- **Comprehensive Display**: Shows all discoverable ComfyUI workflow information
|
||||
- **Export Capabilities**: Download workflow JSON files for reuse
|
||||
- **File Path Integration**: Click to reveal file locations in your system
|
||||
- **Visual Interface**: Clean, organized presentation of complex workflow data
|
||||
|
||||
### Search & Retrieval
|
||||
|
||||
@@ -51,15 +77,23 @@ ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard
|
||||
- **Top-rated prompts** for quality discovery
|
||||
- **Image search** by file properties and metadata
|
||||
|
||||
### 🌐 Web Interface
|
||||
### 🌐 Advanced Web Interface
|
||||
|
||||
- **Admin Dashboard**: Comprehensive browser-based management interface
|
||||
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||
- **Admin Dashboard**: Comprehensive browser-based management interface with modern dark theme
|
||||
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||
- **Real-time Search**: Instant search results as you type
|
||||
- **Bulk Operations**: Edit multiple prompts simultaneously
|
||||
- **Bulk Operations**: Edit multiple prompts simultaneously with batch tools
|
||||
- **Settings Panel**: Configure behavior and display options
|
||||
- **Diagnostics**: Built-in system diagnostics and health checks
|
||||
- **System Management**: Complete toolset for database operations:
|
||||
- **🔍 Metadata Viewer**: Launch standalone PNG analysis tool
|
||||
- **📸 Scan Images**: Bulk scan output directories for ComfyUI images
|
||||
- **💾 Backup/Restore**: Database backup and recovery operations
|
||||
- **🔧 Maintenance**: Database optimization and cleanup tools
|
||||
- **📋 Logs**: Comprehensive logging system with download capability
|
||||
- **🔍 Diagnostics**: System health checks and troubleshooting
|
||||
- **Export Tools**: Download prompts and metadata in various formats
|
||||
- **Progress Tracking**: Real-time progress indicators for long operations
|
||||
- **Error Handling**: Graceful error display with recovery suggestions
|
||||
|
||||
### Database Management
|
||||
|
||||
@@ -96,8 +130,9 @@ ComfyUI Prompt Manager functions as a drop-in replacement for ComfyUI's standard
|
||||
- Use it exactly like the standard "CLIP Text Encode" node
|
||||
|
||||
5. **Access the web interface**:
|
||||
- Open `http://localhost:8188/prompt_manager/admin` in your browser
|
||||
- Or use the built-in ComfyUI interface at `http://localhost:8188/prompt_manager/`
|
||||
- **Admin Dashboard**: `http://localhost:8188/prompt_manager/admin` - Full management interface
|
||||
- **Metadata Viewer**: `http://localhost:8188/prompt_manager/gallery.html` - Standalone PNG analysis tool
|
||||
- **Simple Interface**: `http://localhost:8188/prompt_manager/` - Basic prompt browser
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -131,11 +166,44 @@ The image gallery automatically captures and links generated images:
|
||||
1. **Generate images** using workflows with the Prompt Manager node
|
||||
2. **Open the admin interface** at `http://localhost:8188/prompt_manager/admin`
|
||||
3. **Click the "🖼️ Gallery" button** on any prompt to view its images
|
||||
4. **Click any thumbnail** to open the full-size image viewer
|
||||
4. **Click any thumbnail** to open the full-size image viewer with integrated metadata panel
|
||||
5. **Navigate images** using:
|
||||
- **Arrow keys** (←/→) for keyboard navigation
|
||||
- **Navigation buttons** for mouse control
|
||||
- **ESC key** to close the viewer
|
||||
6. **View metadata** in the right panel showing:
|
||||
- **File path** in output directory
|
||||
- **Model/checkpoint** information
|
||||
- **Prompts** (positive and negative)
|
||||
- **Generation parameters** (steps, CFG, sampler, seed)
|
||||
- **Raw workflow data** with download option
|
||||
|
||||
### 🔬 Analyzing ComfyUI Images
|
||||
|
||||
Use the standalone metadata viewer to analyze any ComfyUI-generated PNG:
|
||||
|
||||
1. **Open the metadata viewer** at `http://localhost:8188/prompt_manager/gallery.html`
|
||||
2. **Drag and drop** any PNG file with ComfyUI workflow data
|
||||
3. **View extracted information**:
|
||||
- **Prompts**: Copy positive/negative prompts with one click
|
||||
- **Parameters**: See all generation settings used
|
||||
- **Model Info**: Identify checkpoint and settings
|
||||
- **Workflow**: Download complete workflow JSON
|
||||
4. **Copy data** using the built-in copy buttons
|
||||
5. **Export workflow** as JSON file for reuse in ComfyUI
|
||||
|
||||
### 📸 Bulk Image Scanning
|
||||
|
||||
Import existing ComfyUI images into your database:
|
||||
|
||||
1. **Open admin dashboard** and click **"📸 Scan Images"**
|
||||
2. **Start scan** to analyze your entire output directory
|
||||
3. **Monitor progress** with real-time status updates
|
||||
4. **Review results** showing:
|
||||
- Images found and processed
|
||||
- Prompts extracted and linked
|
||||
- Any errors or issues encountered
|
||||
5. **Access imported data** through the normal gallery interface
|
||||
|
||||
### 🌐 Web Interface Features
|
||||
|
||||
@@ -260,9 +328,13 @@ CREATE TABLE generated_images (
|
||||
- **`utils/validators.py`** - Input validation and sanitization
|
||||
- **`utils/image_monitor.py`** - Automatic image detection system
|
||||
- **`utils/prompt_tracker.py`** - Prompt execution tracking
|
||||
- **`web/admin.html`** - Comprehensive web management interface
|
||||
- **`utils/metadata_extractor.py`** - PNG metadata analysis engine
|
||||
- **`utils/logging_config.py`** - Comprehensive logging system
|
||||
- **`utils/diagnostics.py`** - System diagnostics and health checks
|
||||
- **`web/admin.html`** - Advanced admin dashboard with metadata panel
|
||||
- **`web/index.html`** - Simple web interface
|
||||
- **`web/prompt_manager.js`** - JavaScript functionality
|
||||
- **`gallery.html`** - Standalone PNG metadata viewer
|
||||
|
||||
### File Structure
|
||||
|
||||
@@ -283,11 +355,15 @@ ComfyUI_PromptManager/
|
||||
│ ├── hashing.py # Hashing utilities
|
||||
│ ├── validators.py # Input validation
|
||||
│ ├── image_monitor.py # Automatic image detection
|
||||
│ └── prompt_tracker.py # Prompt execution tracking
|
||||
│ ├── prompt_tracker.py # Prompt execution tracking
|
||||
│ ├── metadata_extractor.py # PNG metadata analysis
|
||||
│ ├── logging_config.py # Logging system
|
||||
│ └── diagnostics.py # System diagnostics
|
||||
├── web/
|
||||
│ ├── admin.html # Advanced web interface
|
||||
│ ├── admin.html # Advanced admin dashboard
|
||||
│ ├── index.html # Simple web interface
|
||||
│ └── prompt_manager.js # JavaScript functionality
|
||||
├── gallery.html # Standalone metadata viewer
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ └── test_basic.py # Test suite
|
||||
@@ -308,18 +384,35 @@ You can customize the database path by modifying the configuration:
|
||||
DATABASE_PATH = "custom_path/prompts.db"
|
||||
```
|
||||
|
||||
### Image Monitoring Settings
|
||||
### Gallery & Monitoring Settings
|
||||
|
||||
Configure the automatic image detection system:
|
||||
Configure the automatic image detection and gallery system:
|
||||
|
||||
```python
|
||||
# Image monitoring configuration
|
||||
OUTPUT_DIRS = [
|
||||
"/path/to/ComfyUI/output",
|
||||
"/additional/output/directory"
|
||||
]
|
||||
# Gallery monitoring configuration (GalleryConfig class)
|
||||
MONITORING_ENABLED = True
|
||||
MONITORING_DIRECTORIES = [] # Auto-detect ComfyUI output if empty
|
||||
SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.gif']
|
||||
PROCESSING_DELAY = 2.0 # Delay before processing new images
|
||||
PROMPT_EXECUTION_TIMEOUT = 120 # Seconds to wait for prompt linking
|
||||
PROMPT_TIMEOUT = 120 # Seconds to keep prompt context active
|
||||
CLEANUP_INTERVAL = 300 # Seconds between cleanup of expired prompts
|
||||
|
||||
# Performance settings
|
||||
MAX_CONCURRENT_PROCESSING = 3
|
||||
METADATA_EXTRACTION_TIMEOUT = 10 # Seconds for PNG analysis
|
||||
IMAGES_PER_PAGE = 20
|
||||
THUMBNAIL_SIZE = 256
|
||||
```
|
||||
|
||||
### Web Interface Settings
|
||||
|
||||
Configure the web interface behavior:
|
||||
|
||||
```python
|
||||
# Web UI settings (PromptManagerConfig class)
|
||||
RESULT_TIMEOUT = 5 # Seconds to auto-hide results in ComfyUI node
|
||||
WEBUI_DISPLAY_MODE = 'newtab' # 'popup' or 'newtab' for Web UI button
|
||||
SHOW_TEST_BUTTON = False # Show API test button in node UI
|
||||
```
|
||||
|
||||
### Performance Tuning
|
||||
@@ -428,14 +521,26 @@ MIT License - see LICENSE file for details.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Completed in v3.0.0
|
||||
|
||||
- **✅ PNG Metadata Analysis**: Complete ComfyUI workflow extraction from images
|
||||
- **✅ Standalone Metadata Viewer**: Dedicated tool for analyzing any ComfyUI image
|
||||
- **✅ Advanced Admin Dashboard**: Comprehensive management interface with modern UI
|
||||
- **✅ Integrated Metadata Panel**: Real-time workflow analysis in image viewer
|
||||
- **✅ Bulk Image Scanning**: Mass import of existing ComfyUI images
|
||||
- **✅ System Management Tools**: Backup, restore, diagnostics, and maintenance
|
||||
- **✅ Enhanced Error Handling**: Robust PNG parsing with NaN value cleaning
|
||||
|
||||
### Planned Features
|
||||
|
||||
- **☁️ Cloud Sync**: Optional cloud backup and sync
|
||||
- **🤝 Collaboration**: Share prompt collections with other users
|
||||
- **🧠 AI Suggestions**: Recommend similar prompts based on usage
|
||||
- **📈 Advanced Analytics**: Detailed usage statistics and trends
|
||||
- **🔌 Plugin System**: Support for third-party extensions
|
||||
- **🎨 Batch Processing**: Bulk image operations and metadata editing
|
||||
- **🧠 AI Suggestions**: Recommend similar prompts based on metadata analysis
|
||||
- **📈 Advanced Analytics**: Detailed usage statistics and trends with workflow insights
|
||||
- **🔌 Plugin System**: Support for third-party extensions and custom analyzers
|
||||
- **🎨 Enhanced Batch Processing**: Advanced bulk operations with metadata editing
|
||||
- **🔄 Workflow Templates**: Save and reuse common workflow patterns
|
||||
- **📊 Visual Analytics**: Charts and graphs for prompt effectiveness analysis
|
||||
|
||||
### Integration Ideas
|
||||
|
||||
@@ -446,14 +551,27 @@ MIT License - see LICENSE file for details.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.0.0 (Metadata Analysis Release)
|
||||
|
||||
- **🔬 PNG Metadata Analysis**: Advanced ComfyUI workflow extraction from PNG images
|
||||
- **📋 Standalone Metadata Viewer**: Dedicated tool at `/gallery.html` for analyzing any ComfyUI image
|
||||
- **🖼️ Integrated Metadata Panel**: Real-time workflow analysis in the image viewer
|
||||
- **📸 Bulk Image Scanning**: Mass import and analysis of existing ComfyUI output directories
|
||||
- **🛠️ System Management Suite**: Complete backup, restore, maintenance, and diagnostic tools
|
||||
- **📋 Advanced Logging**: Comprehensive logging system with web-based log viewer
|
||||
- **🎨 Enhanced Admin Dashboard**: Modern dark theme with improved UX and management tools
|
||||
- **🔧 Robust Error Handling**: Advanced PNG parsing with NaN value cleaning and error recovery
|
||||
- **📁 File Path Integration**: Direct file path display and system integration
|
||||
- **⚡ Performance Optimizations**: Improved metadata extraction and database operations
|
||||
|
||||
### v2.0.0 (Gallery Release)
|
||||
|
||||
- **🖼️ Automatic Image Gallery**: Complete image tracking and gallery system
|
||||
- **🌐 Advanced Web Interface**: Comprehensive admin dashboard with responsive design
|
||||
- **📱 Image Viewer**: Full-screen modal with navigation and keyboard shortcuts
|
||||
- **🔍 Enhanced Search**: Real-time search with advanced filtering options
|
||||
- **⚡ Performance Improvements**: Optimized database operations and NaN value handling
|
||||
- **🛠️ Diagnostics**: Built-in system diagnostics and health monitoring
|
||||
- **⚡ Performance Improvements**: Optimized database operations
|
||||
- **🛠️ Basic Diagnostics**: Initial system diagnostics and health monitoring
|
||||
- **📊 Bulk Operations**: Multi-select prompt editing and management
|
||||
|
||||
### v1.0.0 (Initial Release)
|
||||
|
||||
+558
@@ -0,0 +1,558 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<style>
|
||||
/* Ensure the layout takes full height and prevents overflow */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ensure image container uses flexbox properly */
|
||||
#image-area {
|
||||
min-height: 0; /* Allow flex child to shrink */
|
||||
}
|
||||
|
||||
/* Make sure the image scales properly */
|
||||
#main-image {
|
||||
max-width: calc(100% - 2rem); /* Account for padding */
|
||||
max-height: calc(100% - 2rem); /* Account for padding */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let currentWorkflowData = null;
|
||||
|
||||
function openInFileExplorer(path) {
|
||||
// For Windows file explorer
|
||||
if (navigator.platform.indexOf('Win') > -1) {
|
||||
// This will work in Electron apps or desktop environments
|
||||
try {
|
||||
window.require('child_process').exec(`explorer /select,"${path}"`);
|
||||
} catch (e) {
|
||||
// Fallback: copy path to clipboard and show alert
|
||||
navigator.clipboard.writeText(path).then(() => {
|
||||
alert('File path copied to clipboard: ' + path);
|
||||
}).catch(() => {
|
||||
alert('File path: ' + path);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// For other platforms, just copy to clipboard
|
||||
navigator.clipboard.writeText(path).then(() => {
|
||||
alert('File path copied to clipboard: ' + path);
|
||||
}).catch(() => {
|
||||
alert('File path: ' + path);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard(text, button) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
// Show temporary feedback
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '✓ Copied!';
|
||||
button.classList.add('text-green-400');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
button.classList.remove('text-green-400');
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
alert('Copy failed: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
function copyPositivePrompt(event) {
|
||||
if (currentWorkflowData && currentWorkflowData.positivePrompt) {
|
||||
copyToClipboard(currentWorkflowData.positivePrompt, event.target.closest('button'));
|
||||
}
|
||||
}
|
||||
|
||||
function copyNegativePrompt(event) {
|
||||
if (currentWorkflowData && currentWorkflowData.negativePrompt) {
|
||||
copyToClipboard(currentWorkflowData.negativePrompt, event.target.closest('button'));
|
||||
}
|
||||
}
|
||||
|
||||
function downloadWorkflowJSON() {
|
||||
if (currentWorkflowData && currentWorkflowData.workflow) {
|
||||
const dataStr = JSON.stringify(currentWorkflowData.workflow, null, 2);
|
||||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'comfyui_workflow.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAllData() {
|
||||
if (!currentWorkflowData) return;
|
||||
|
||||
const allData = `Checkpoint: ${currentWorkflowData.checkpoint || 'Unknown'}
|
||||
Positive Prompt: ${currentWorkflowData.positivePrompt}
|
||||
Negative Prompt: ${currentWorkflowData.negativePrompt}
|
||||
Steps: ${currentWorkflowData.steps || 'Unknown'}
|
||||
CFG Scale: ${currentWorkflowData.cfgScale || 'Unknown'}
|
||||
Sampler: ${currentWorkflowData.sampler || 'Unknown'}
|
||||
Seed: ${currentWorkflowData.seed || 'Unknown'}`;
|
||||
|
||||
await copyToClipboard(allData);
|
||||
}
|
||||
|
||||
async function parsePNGMetadata(arrayBuffer) {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
let offset = 8; // Skip PNG signature
|
||||
const metadata = {};
|
||||
|
||||
while (offset < arrayBuffer.byteLength - 8) {
|
||||
const length = dataView.getUint32(offset);
|
||||
const type = new TextDecoder().decode(arrayBuffer.slice(offset + 4, offset + 8));
|
||||
|
||||
if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') {
|
||||
const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + length);
|
||||
let text;
|
||||
|
||||
if (type === 'tEXt') {
|
||||
text = new TextDecoder().decode(chunkData);
|
||||
} else if (type === 'iTXt') {
|
||||
// iTXt format: keyword\0compression\0language\0translated_keyword\0text
|
||||
const textData = new TextDecoder().decode(chunkData);
|
||||
const parts = textData.split('\0');
|
||||
if (parts.length >= 5) {
|
||||
metadata[parts[0]] = parts[4];
|
||||
}
|
||||
text = textData;
|
||||
} else if (type === 'zTXt') {
|
||||
// zTXt is compressed - basic parsing (might need proper decompression)
|
||||
text = new TextDecoder().decode(chunkData);
|
||||
}
|
||||
|
||||
// Parse the text chunk for key-value pairs
|
||||
const nullIndex = text.indexOf('\0');
|
||||
if (nullIndex !== -1) {
|
||||
const key = text.substring(0, nullIndex);
|
||||
const value = text.substring(nullIndex + 1);
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
offset += 8 + length + 4; // Move to next chunk (8 = length + type, 4 = CRC)
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function extractComfyUIData(metadata) {
|
||||
// Look for ComfyUI workflow data in various possible fields
|
||||
let workflowData = null;
|
||||
let promptData = null;
|
||||
|
||||
// Common ComfyUI metadata field names
|
||||
const workflowFields = ['workflow', 'Workflow', 'comfy', 'ComfyUI'];
|
||||
const promptFields = ['prompt', 'Prompt', 'parameters', 'Parameters'];
|
||||
|
||||
for (const field of workflowFields) {
|
||||
if (metadata[field]) {
|
||||
try {
|
||||
workflowData = JSON.parse(metadata[field]);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log('Failed to parse workflow field:', field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of promptFields) {
|
||||
if (metadata[field]) {
|
||||
try {
|
||||
promptData = JSON.parse(metadata[field]);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log('Failed to parse prompt field:', field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { workflow: workflowData, prompt: promptData };
|
||||
}
|
||||
|
||||
function updateDataPanel(comfyData, filename) {
|
||||
const dataPanel = document.getElementById('data-panel-content');
|
||||
if (!dataPanel) return;
|
||||
|
||||
// Extract information from ComfyUI workflow
|
||||
let checkpoint = 'Unknown';
|
||||
let positivePrompt = 'No prompt found';
|
||||
let negativePrompt = 'No negative prompt found';
|
||||
let steps = 'Unknown';
|
||||
let cfgScale = 'Unknown';
|
||||
let sampler = 'Unknown';
|
||||
let seed = 'Unknown';
|
||||
|
||||
// Parse prompt data first (more reliable for actual generation parameters)
|
||||
if (comfyData.prompt) {
|
||||
console.log('Parsing prompt data...');
|
||||
const promptNodes = comfyData.prompt;
|
||||
|
||||
for (const nodeId in promptNodes) {
|
||||
const node = promptNodes[nodeId];
|
||||
console.log(`Node ${nodeId}:`, node.class_type, node);
|
||||
|
||||
// Checkpoint
|
||||
if (node.class_type === 'CheckpointLoaderSimple' && node.inputs) {
|
||||
checkpoint = node.inputs.ckpt_name || checkpoint;
|
||||
}
|
||||
|
||||
// Prompts - need to identify which is positive vs negative
|
||||
if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) {
|
||||
// PromptManager typically contains the positive prompt
|
||||
positivePrompt = node.inputs.text;
|
||||
console.log('Found PromptManager with text:', positivePrompt.substring(0, 100));
|
||||
}
|
||||
|
||||
if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) {
|
||||
// Check if this looks like a negative prompt
|
||||
const text = node.inputs.text.toLowerCase();
|
||||
if (text.includes('bad anatomy') || text.includes('unfinished') ||
|
||||
text.includes('censored') || text.includes('weird anatomy') ||
|
||||
text.includes('negative') || text.includes('embedding:')) {
|
||||
negativePrompt = node.inputs.text;
|
||||
console.log('Found negative prompt:', negativePrompt.substring(0, 100));
|
||||
} else if (positivePrompt === 'No prompt found') {
|
||||
// If we haven't found a positive prompt yet, this might be it
|
||||
positivePrompt = node.inputs.text;
|
||||
console.log('Found potential positive prompt:', positivePrompt.substring(0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Sampling parameters
|
||||
if (node.class_type === 'KSampler' && node.inputs) {
|
||||
seed = node.inputs.seed || seed;
|
||||
steps = node.inputs.steps || steps;
|
||||
cfgScale = node.inputs.cfg || cfgScale;
|
||||
sampler = node.inputs.sampler_name || sampler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to workflow parsing if prompt parsing didn't work well
|
||||
if (comfyData.workflow && (positivePrompt === 'No prompt found' || checkpoint === 'Unknown')) {
|
||||
console.log('Falling back to workflow parsing...');
|
||||
const nodes = comfyData.workflow.nodes || [];
|
||||
|
||||
// Look for checkpoint loader
|
||||
const checkpointNode = nodes.find(node =>
|
||||
node.type === 'CheckpointLoaderSimple' ||
|
||||
node.type === 'CheckpointLoader'
|
||||
);
|
||||
if (checkpointNode && checkpointNode.widgets_values) {
|
||||
checkpoint = checkpointNode.widgets_values[0] || checkpoint;
|
||||
}
|
||||
|
||||
// Look for prompts in workflow nodes
|
||||
const textEncodeNodes = nodes.filter(node => node.type === 'CLIPTextEncode');
|
||||
const promptManagerNodes = nodes.filter(node => node.type === 'PromptManager');
|
||||
|
||||
// Check PromptManager first for positive prompt
|
||||
if (promptManagerNodes.length > 0 && promptManagerNodes[0].widgets_values) {
|
||||
positivePrompt = promptManagerNodes[0].widgets_values[0] || positivePrompt;
|
||||
}
|
||||
|
||||
// For negative prompt, look for CLIPTextEncode that isn't the positive
|
||||
for (const node of textEncodeNodes) {
|
||||
if (node.widgets_values && node.widgets_values[0]) {
|
||||
const text = node.widgets_values[0].toLowerCase();
|
||||
if (text.includes('bad anatomy') || text.includes('unfinished') ||
|
||||
text.includes('censored') || text.includes('weird anatomy')) {
|
||||
negativePrompt = node.widgets_values[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for sampler
|
||||
const samplerNode = nodes.find(node =>
|
||||
node.type === 'KSampler' ||
|
||||
node.type === 'KSamplerAdvanced'
|
||||
);
|
||||
if (samplerNode && samplerNode.widgets_values) {
|
||||
seed = samplerNode.widgets_values[0] || seed;
|
||||
steps = samplerNode.widgets_values[1] || steps;
|
||||
cfgScale = samplerNode.widgets_values[2] || cfgScale;
|
||||
sampler = samplerNode.widgets_values[3] || sampler;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the HTML
|
||||
dataPanel.innerHTML = `
|
||||
<!-- File Path -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">File Path</h2>
|
||||
<div class="text-sm text-blue-400 hover:text-blue-300 cursor-pointer bg-gray-800 p-2 rounded break-all" onclick="openInFileExplorer('${filename}')">
|
||||
${filename}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources used -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">Resources used</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-blue-400 hover:text-blue-300 cursor-pointer">${checkpoint}</div>
|
||||
<div class="text-xs text-gray-500">ComfyUI Generated</div>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CHECKPOINT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h2 class="text-sm font-medium text-gray-300">Prompt</h2>
|
||||
<span class="px-2 py-1 text-xs bg-orange-600 text-orange-100 rounded">COMFYUI</span>
|
||||
<button class="ml-auto text-gray-400 hover:text-gray-300" onclick="copyPositivePrompt(event)">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
||||
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
${positivePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 text-sm mt-1" onclick="showFullPrompt(\'positive\')">Show more</button>' : ''}
|
||||
</div>
|
||||
|
||||
<!-- Negative prompt -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-sm font-medium text-gray-300">Negative prompt</h2>
|
||||
<button class="text-gray-400 hover:text-gray-300" onclick="copyNegativePrompt(event)">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
||||
${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
${negativePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 text-sm mt-1" onclick="showFullPrompt(\'negative\')">Show more</button>' : ''}
|
||||
</div>
|
||||
|
||||
<!-- Other metadata -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-3">Other metadata</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CFG SCALE: ${cfgScale}</span>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">STEPS: ${steps}</span>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SAMPLER: ${sampler}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SEED: ${seed}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Workflow Data -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">ComfyUI Workflow</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="text-blue-400 hover:text-blue-300 text-sm" onclick="showWorkflowData()">View Raw Workflow JSON</button>
|
||||
<button class="text-blue-400 hover:text-blue-300" onclick="downloadWorkflowJSON()" title="Download JSON">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store the full data for later use
|
||||
currentWorkflowData = {
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
checkpoint,
|
||||
steps,
|
||||
cfgScale,
|
||||
sampler,
|
||||
seed,
|
||||
workflow: comfyData.workflow,
|
||||
prompt: comfyData.prompt
|
||||
};
|
||||
}
|
||||
|
||||
function showFullPrompt(type) {
|
||||
if (currentWorkflowData) {
|
||||
const prompt = type === 'positive' ? currentWorkflowData.positivePrompt : currentWorkflowData.negativePrompt;
|
||||
const newWindow = window.open('', '_blank');
|
||||
newWindow.document.write(`
|
||||
<html>
|
||||
<head><title>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</title></head>
|
||||
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
|
||||
<h2>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</h2>
|
||||
<div style="background: #222; padding: 15px; border-radius: 5px; white-space: pre-wrap; line-height: 1.5;">${prompt}</div>
|
||||
<button onclick="navigator.clipboard.writeText(\`${prompt.replace(/`/g, '\\`')}\`).then(() => alert('Copied!'))" style="margin-top: 20px; padding: 10px 20px; background: #444; color: #fff; border: none; border-radius: 5px; cursor: pointer;">Copy to Clipboard</button>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function showWorkflowData() {
|
||||
if (currentWorkflowData && currentWorkflowData.workflow) {
|
||||
const newWindow = window.open('', '_blank');
|
||||
newWindow.document.write(`
|
||||
<html>
|
||||
<head><title>ComfyUI Workflow Data</title></head>
|
||||
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
|
||||
<h2>ComfyUI Workflow JSON</h2>
|
||||
<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${JSON.stringify(currentWorkflowData.workflow, null, 2)}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImageUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const metadata = await parsePNGMetadata(arrayBuffer);
|
||||
const comfyData = extractComfyUIData(metadata);
|
||||
|
||||
// Update the image display
|
||||
const img = document.getElementById('main-image');
|
||||
const url = URL.createObjectURL(file);
|
||||
img.src = url;
|
||||
|
||||
// Update data panel
|
||||
updateDataPanel(comfyData, file.name);
|
||||
|
||||
console.log('Extracted metadata:', metadata);
|
||||
console.log('ComfyUI data:', comfyData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
alert('Error processing image: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize drag and drop
|
||||
function initializeDragDrop() {
|
||||
const imageArea = document.getElementById('image-area');
|
||||
|
||||
imageArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
imageArea.classList.add('bg-gray-900');
|
||||
});
|
||||
|
||||
imageArea.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
imageArea.classList.remove('bg-gray-900');
|
||||
});
|
||||
|
||||
imageArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
imageArea.classList.remove('bg-gray-900');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'image/png') {
|
||||
const event = { target: { files: [file] } };
|
||||
handleImageUpload(event);
|
||||
} else {
|
||||
alert('Please drop a PNG file');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
window.addEventListener('load', initializeDragDrop);
|
||||
|
||||
// Handle window resize to ensure image stays properly sized
|
||||
window.addEventListener('resize', () => {
|
||||
// The CSS will handle the responsive sizing automatically
|
||||
// but we can trigger any additional adjustments if needed
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-black text-gray-100 font-sans overflow-hidden">
|
||||
<div class="h-screen flex">
|
||||
<!-- Main Image Area -->
|
||||
<div id="image-area" class="flex-1 flex items-center justify-center p-4 bg-black relative border-2 border-dashed border-transparent transition-colors min-w-0">
|
||||
<!-- Upload prompt overlay -->
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center text-gray-500 pointer-events-none">
|
||||
<svg class="w-16 h-16 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||
</svg>
|
||||
<p class="text-lg mb-2">Drag & Drop PNG with ComfyUI Workflow</p>
|
||||
<p class="text-sm">Or click to browse files</p>
|
||||
</div>
|
||||
|
||||
<!-- File input -->
|
||||
<input type="file" id="file-input" accept=".png" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" onchange="handleImageUpload(event)">
|
||||
|
||||
<!-- Navigation arrows -->
|
||||
<button class="absolute left-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-gray-800 bg-opacity-50 hover:bg-opacity-75 rounded-full flex items-center justify-center text-white transition-all z-10">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Main Image -->
|
||||
<div class="w-full h-full flex items-center justify-center z-10 pointer-events-none">
|
||||
<img id="main-image"
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='1200' viewBox='0 0 800 1200'%3E%3Crect width='800' height='1200' fill='%23374151'/%3E%3Ctext x='400' y='600' font-family='Arial' font-size='24' fill='%23D1D5DB' text-anchor='middle'%3EDrop PNG Here%3C/text%3E%3Ctext x='400' y='640' font-family='Arial' font-size='16' fill='%239CA3AF' text-anchor='middle'%3EComfyUI Workflow%3C/text%3E%3C/svg%3E"
|
||||
alt="Generated Image"
|
||||
class="max-w-full max-h-full object-contain rounded shadow-2xl">
|
||||
</div>
|
||||
|
||||
<button class="absolute right-4 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-gray-800 bg-opacity-50 hover:bg-opacity-75 rounded-full flex items-center justify-center text-white transition-all z-10">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Generation Data Panel -->
|
||||
<div class="w-96 bg-gray-900 border-l border-gray-700 flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h1 class="text-lg font-medium text-white">Generation data</h1>
|
||||
</div>
|
||||
<button class="text-xs text-blue-400 hover:text-blue-300 transition-colors" onclick="copyAllData()">
|
||||
📋 COPY ALL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div id="data-panel-content" class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<!-- Initial placeholder content -->
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p class="text-lg mb-2">No Image Loaded</p>
|
||||
<p class="text-sm">Upload a PNG with embedded ComfyUI workflow to view generation data</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -122,6 +122,38 @@ class PromptManagerAPI:
|
||||
status=500,
|
||||
)
|
||||
|
||||
# Serve the gallery interface
|
||||
@routes.get("/prompt_manager/gallery.html")
|
||||
async def serve_gallery_ui(request):
|
||||
try:
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
html_path = os.path.join(current_dir, "gallery.html")
|
||||
|
||||
if os.path.exists(html_path):
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
|
||||
return web.Response(
|
||||
text=html_content, content_type="text/html", charset="utf-8"
|
||||
)
|
||||
else:
|
||||
return web.Response(
|
||||
text="<h1>Gallery not found</h1><p>gallery.html file not located at expected path.</p>",
|
||||
content_type="text/html",
|
||||
status=404,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return web.Response(
|
||||
text=f"<h1>Error</h1><p>Failed to load gallery: {str(e)}</p>",
|
||||
content_type="text/html",
|
||||
status=500,
|
||||
)
|
||||
|
||||
# Serve the admin interface
|
||||
@routes.get("/prompt_manager/admin")
|
||||
async def serve_admin_ui(request):
|
||||
@@ -623,10 +655,16 @@ class PromptManagerAPI:
|
||||
async def get_settings(self, request):
|
||||
"""Get current settings."""
|
||||
try:
|
||||
# For now, return default settings
|
||||
return web.json_response(
|
||||
{"success": True, "settings": {"result_timeout": 5}}
|
||||
)
|
||||
from .config import PromptManagerConfig
|
||||
|
||||
# Return actual configuration settings
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"settings": {
|
||||
"result_timeout": PromptManagerConfig.RESULT_TIMEOUT,
|
||||
"webui_display_mode": PromptManagerConfig.WEBUI_DISPLAY_MODE
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to get settings: {str(e)}"},
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ class PromptManagerConfig:
|
||||
# Web UI settings
|
||||
RESULT_TIMEOUT = 5 # Seconds to auto-hide results in ComfyUI node
|
||||
SHOW_TEST_BUTTON = False # Show API test button in node UI
|
||||
WEBUI_DISPLAY_MODE = 'popup' # 'popup' or 'newtab'
|
||||
WEBUI_DISPLAY_MODE = 'newtab' # 'popup' or 'newtab'
|
||||
|
||||
# Performance settings
|
||||
MAX_SEARCH_RESULTS = 100
|
||||
|
||||
+480
-34
@@ -196,6 +196,10 @@
|
||||
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>
|
||||
<button id="metadataBtn"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
🔍 Metadata
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -568,41 +572,79 @@
|
||||
|
||||
<!-- Image Viewer Modal -->
|
||||
<div id="imageViewerModal" class="fixed inset-0 bg-black bg-opacity-90 hidden items-center justify-center" style="z-index: 9999;">
|
||||
<div class="relative w-full h-full flex items-center justify-center">
|
||||
<!-- Close button -->
|
||||
<button onclick="window.admin.closeImageViewer()" class="absolute top-4 right-4 p-2 rounded-lg bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" 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>
|
||||
|
||||
<!-- Previous button -->
|
||||
<button onclick="window.admin.previousImage()" class="absolute left-4 top-1/2 transform -translate-y-1/2 p-3 rounded-full bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Next button -->
|
||||
<button onclick="window.admin.nextImage()" class="absolute right-4 top-1/2 transform -translate-y-1/2 p-3 rounded-full bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- View mode indicator -->
|
||||
<div id="viewModeIndicator" class="absolute top-4 left-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full z-10">
|
||||
Fit to Window
|
||||
<div class="h-screen flex w-full">
|
||||
<!-- Main Image Area -->
|
||||
<div class="flex-1 flex items-center justify-center p-4 bg-black relative">
|
||||
<!-- Close button -->
|
||||
<button onclick="window.admin.closeImageViewer()" class="absolute top-4 right-4 p-2 rounded-lg bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" 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>
|
||||
|
||||
<!-- Previous button -->
|
||||
<button onclick="window.admin.previousImage()" class="absolute left-4 top-1/2 transform -translate-y-1/2 p-3 rounded-full bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Next button -->
|
||||
<button onclick="window.admin.nextImage()" class="absolute right-4 top-1/2 transform -translate-y-1/2 p-3 rounded-full bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- View mode indicator -->
|
||||
<div id="viewModeIndicator" class="absolute top-4 left-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full z-10">
|
||||
Fit to Window
|
||||
</div>
|
||||
|
||||
<!-- Toggle metadata panel button -->
|
||||
<button id="toggleMetadataBtn" onclick="window.admin.toggleMetadataPanel()" class="absolute top-4 left-1/2 transform -translate-x-1/2 p-2 rounded-lg bg-black/50 hover:bg-black/70 transition-colors z-10">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Image container -->
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<img id="viewerImage" src="" alt="Generated image" class="max-w-full max-h-full object-contain rounded-lg cursor-pointer transition-all duration-300" onclick="window.admin.toggleImageSize()">
|
||||
</div>
|
||||
|
||||
<!-- Image counter -->
|
||||
<div id="imageCounter" class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-white text-sm bg-black/50 px-3 py-1 rounded-full z-10">
|
||||
1 / 1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image container -->
|
||||
<div class="w-full h-full flex items-center justify-center p-4">
|
||||
<img id="viewerImage" src="" alt="Generated image" class="rounded-lg cursor-pointer transition-all duration-300" onclick="window.admin.toggleImageSize()">
|
||||
</div>
|
||||
|
||||
<!-- Image counter -->
|
||||
<div id="imageCounter" class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-white text-sm bg-black/50 px-3 py-1 rounded-full z-10">
|
||||
1 / 1
|
||||
|
||||
<!-- Metadata Panel -->
|
||||
<div id="metadataPanel" class="w-96 bg-gray-900 border-l border-gray-700 flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h1 class="text-lg font-medium text-white">Metadata</h1>
|
||||
</div>
|
||||
<button class="text-xs text-blue-400 hover:text-blue-300 transition-colors" onclick="window.admin.copyAllMetadata()">
|
||||
📋 COPY ALL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div id="metadataContent" class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<!-- Metadata will be loaded here -->
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p class="text-lg mb-2">No Metadata</p>
|
||||
<p class="text-sm">Loading image metadata...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -847,6 +889,7 @@
|
||||
document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
|
||||
document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal());
|
||||
document.getElementById("logsBtn").addEventListener("click", () => this.showLogsModal());
|
||||
document.getElementById("metadataBtn").addEventListener("click", () => this.openMetadataViewer());
|
||||
|
||||
// Pagination controls
|
||||
document.getElementById("limitSelector").addEventListener("change", (e) => this.changeLimit(parseInt(e.target.value)));
|
||||
@@ -1702,6 +1745,8 @@
|
||||
this.naturalImageSize.width = img.naturalWidth;
|
||||
this.naturalImageSize.height = img.naturalHeight;
|
||||
this.applyImageSizing();
|
||||
// Extract metadata after image loads
|
||||
this.extractImageMetadata();
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
@@ -1724,6 +1769,8 @@
|
||||
this.naturalImageSize.width = img.naturalWidth;
|
||||
this.naturalImageSize.height = img.naturalHeight;
|
||||
this.applyImageSizing();
|
||||
// Extract metadata after image loads
|
||||
this.extractImageMetadata();
|
||||
};
|
||||
|
||||
img.src = nextImageUrl;
|
||||
@@ -1743,6 +1790,8 @@
|
||||
this.naturalImageSize.width = img.naturalWidth;
|
||||
this.naturalImageSize.height = img.naturalHeight;
|
||||
this.applyImageSizing();
|
||||
// Extract metadata after image loads
|
||||
this.extractImageMetadata();
|
||||
};
|
||||
|
||||
img.src = prevImageUrl;
|
||||
@@ -2759,6 +2808,403 @@
|
||||
this.pagination.currentPage = 1; // Reset to first page
|
||||
await this.loadRecentPrompts(1);
|
||||
}
|
||||
|
||||
// Metadata functionality
|
||||
openMetadataViewer() {
|
||||
window.open('/prompt_manager/gallery.html', '_blank');
|
||||
}
|
||||
|
||||
toggleMetadataPanel() {
|
||||
const panel = document.getElementById('metadataPanel');
|
||||
panel.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
async extractImageMetadata() {
|
||||
const viewerImage = document.getElementById('viewerImage');
|
||||
const imageSrc = viewerImage.src;
|
||||
|
||||
if (!imageSrc) return;
|
||||
|
||||
try {
|
||||
// Fetch the image as an array buffer
|
||||
const response = await fetch(imageSrc);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
console.log('Image buffer size:', arrayBuffer.byteLength);
|
||||
|
||||
// Parse PNG metadata
|
||||
const metadata = await this.parsePNGMetadata(arrayBuffer);
|
||||
console.log('Extracted metadata:', metadata);
|
||||
|
||||
const comfyData = this.extractComfyUIData(metadata);
|
||||
console.log('ComfyUI data:', comfyData);
|
||||
|
||||
// Update metadata panel
|
||||
this.updateMetadataPanel(comfyData, imageSrc);
|
||||
} catch (error) {
|
||||
console.error('Error extracting metadata:', error);
|
||||
this.showMetadataError();
|
||||
}
|
||||
}
|
||||
|
||||
async parsePNGMetadata(arrayBuffer) {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
let offset = 8; // Skip PNG signature
|
||||
const metadata = {};
|
||||
let chunkCount = 0;
|
||||
|
||||
console.log('Starting PNG metadata parsing...');
|
||||
|
||||
while (offset < arrayBuffer.byteLength - 8) {
|
||||
const length = dataView.getUint32(offset);
|
||||
const type = new TextDecoder().decode(arrayBuffer.slice(offset + 4, offset + 8));
|
||||
|
||||
chunkCount++;
|
||||
console.log(`Chunk ${chunkCount}: type=${type}, length=${length}`);
|
||||
|
||||
if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') {
|
||||
const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + length);
|
||||
let text;
|
||||
|
||||
if (type === 'tEXt') {
|
||||
text = new TextDecoder().decode(chunkData);
|
||||
} else if (type === 'iTXt') {
|
||||
// iTXt format: keyword\0compression\0language\0translated_keyword\0text
|
||||
const textData = new TextDecoder().decode(chunkData);
|
||||
const parts = textData.split('\0');
|
||||
console.log(`iTXt parts count: ${parts.length}, first part: ${parts[0]}`);
|
||||
if (parts.length >= 5) {
|
||||
metadata[parts[0]] = parts[4];
|
||||
}
|
||||
text = textData;
|
||||
} else if (type === 'zTXt') {
|
||||
// zTXt is compressed - basic parsing (might need proper decompression)
|
||||
text = new TextDecoder().decode(chunkData);
|
||||
}
|
||||
|
||||
// Parse the text chunk for key-value pairs
|
||||
const nullIndex = text.indexOf('\0');
|
||||
if (nullIndex !== -1) {
|
||||
const key = text.substring(0, nullIndex);
|
||||
const value = text.substring(nullIndex + 1);
|
||||
console.log(`Found metadata: ${key} = ${value.substring(0, 100)}...`);
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
offset += 8 + length + 4; // Move to next chunk (8 = length + type, 4 = CRC)
|
||||
}
|
||||
|
||||
console.log(`Parsed ${chunkCount} chunks, found ${Object.keys(metadata).length} metadata items`);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
extractComfyUIData(metadata) {
|
||||
// Look for ComfyUI workflow data in various possible fields
|
||||
let workflowData = null;
|
||||
let promptData = null;
|
||||
|
||||
// Common ComfyUI metadata field names
|
||||
const workflowFields = ['workflow', 'Workflow', 'comfy', 'ComfyUI'];
|
||||
const promptFields = ['prompt', 'Prompt', 'parameters', 'Parameters'];
|
||||
|
||||
for (const field of workflowFields) {
|
||||
if (metadata[field]) {
|
||||
try {
|
||||
// Clean NaN values from JSON string before parsing
|
||||
let cleanedJson = metadata[field];
|
||||
cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null');
|
||||
cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null');
|
||||
|
||||
workflowData = JSON.parse(cleanedJson);
|
||||
console.log(`Successfully parsed workflow field: ${field}`);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log('Failed to parse workflow field:', field, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of promptFields) {
|
||||
if (metadata[field]) {
|
||||
try {
|
||||
// Clean NaN values from JSON string before parsing
|
||||
let cleanedJson = metadata[field];
|
||||
cleanedJson = cleanedJson.replace(/:\s*NaN\b/g, ': null');
|
||||
cleanedJson = cleanedJson.replace(/\bNaN\b/g, 'null');
|
||||
|
||||
promptData = JSON.parse(cleanedJson);
|
||||
console.log(`Successfully parsed prompt field: ${field}`);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log('Failed to parse prompt field:', field, e.message);
|
||||
console.log('Raw data:', metadata[field].substring(0, 200) + '...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { workflow: workflowData, prompt: promptData };
|
||||
}
|
||||
|
||||
updateMetadataPanel(comfyData, imageSrc) {
|
||||
const metadataContent = document.getElementById('metadataContent');
|
||||
if (!metadataContent) return;
|
||||
|
||||
// Get the actual file path from current image data
|
||||
let filePath = imageSrc; // fallback to URL
|
||||
if (this.currentGalleryImages && this.currentImageIndex !== null && this.currentGalleryImages[this.currentImageIndex]) {
|
||||
const currentImage = this.currentGalleryImages[this.currentImageIndex];
|
||||
filePath = currentImage.image_path || currentImage.filename || imageSrc;
|
||||
}
|
||||
|
||||
// Extract information from ComfyUI workflow
|
||||
let checkpoint = 'Unknown';
|
||||
let positivePrompt = 'No prompt found';
|
||||
let negativePrompt = 'No negative prompt found';
|
||||
let steps = 'Unknown';
|
||||
let cfgScale = 'Unknown';
|
||||
let sampler = 'Unknown';
|
||||
let seed = 'Unknown';
|
||||
|
||||
// Parse prompt data first (more reliable for actual generation parameters)
|
||||
if (comfyData.prompt) {
|
||||
console.log('Parsing prompt data...', Object.keys(comfyData.prompt).length, 'nodes');
|
||||
const promptNodes = comfyData.prompt;
|
||||
|
||||
for (const nodeId in promptNodes) {
|
||||
const node = promptNodes[nodeId];
|
||||
console.log(`Node ${nodeId}:`, node.class_type, node);
|
||||
|
||||
// Checkpoint
|
||||
if (node.class_type === 'CheckpointLoaderSimple' && node.inputs) {
|
||||
checkpoint = node.inputs.ckpt_name || checkpoint;
|
||||
console.log('Found checkpoint:', checkpoint);
|
||||
}
|
||||
|
||||
// Prompts - need to identify which is positive vs negative
|
||||
if (node.class_type === 'PromptManager' && node.inputs && node.inputs.text) {
|
||||
// PromptManager typically contains the positive prompt
|
||||
positivePrompt = node.inputs.text;
|
||||
console.log('Found PromptManager with text:', positivePrompt.substring(0, 100));
|
||||
}
|
||||
|
||||
if (node.class_type === 'CLIPTextEncode' && node.inputs && node.inputs.text) {
|
||||
console.log('Found CLIPTextEncode:', node.inputs.text.substring(0, 50));
|
||||
// Check if this looks like a negative prompt
|
||||
const text = node.inputs.text.toLowerCase();
|
||||
if (text.includes('bad anatomy') || text.includes('unfinished') ||
|
||||
text.includes('censored') || text.includes('weird anatomy') ||
|
||||
text.includes('negative') || text.includes('embedding:')) {
|
||||
negativePrompt = node.inputs.text;
|
||||
console.log('Found negative prompt:', negativePrompt.substring(0, 100));
|
||||
} else if (positivePrompt === 'No prompt found') {
|
||||
// If we haven't found a positive prompt yet, this might be it
|
||||
positivePrompt = node.inputs.text;
|
||||
console.log('Found potential positive prompt:', positivePrompt.substring(0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Sampling parameters
|
||||
if (node.class_type === 'KSampler' && node.inputs) {
|
||||
seed = node.inputs.seed || seed;
|
||||
steps = node.inputs.steps || steps;
|
||||
cfgScale = node.inputs.cfg || cfgScale;
|
||||
sampler = node.inputs.sampler_name || sampler;
|
||||
console.log('Found sampler params:', {seed, steps, cfgScale, sampler});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('No prompt data found, will try workflow fallback');
|
||||
}
|
||||
|
||||
// Store the current metadata for copying
|
||||
this.currentMetadata = {
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
checkpoint,
|
||||
steps,
|
||||
cfgScale,
|
||||
sampler,
|
||||
seed,
|
||||
workflow: comfyData.workflow,
|
||||
prompt: comfyData.prompt
|
||||
};
|
||||
|
||||
// Update the HTML
|
||||
metadataContent.innerHTML = `
|
||||
<!-- File Path -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">File Path</h2>
|
||||
<div class="text-sm text-blue-400 hover:text-blue-300 cursor-pointer bg-gray-800 p-2 rounded break-all" onclick="window.admin.copyToClipboard('${filePath}')">
|
||||
${filePath}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources used -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">Resources used</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-blue-400 hover:text-blue-300 cursor-pointer">${checkpoint}</div>
|
||||
<div class="text-xs text-gray-500">ComfyUI Generated</div>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CHECKPOINT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h2 class="text-sm font-medium text-gray-300">Prompt</h2>
|
||||
<span class="px-2 py-1 text-xs bg-orange-600 text-orange-100 rounded">COMFYUI</span>
|
||||
<button class="ml-auto text-gray-400 hover:text-gray-300" onclick="window.admin.copyPrompt('positive')">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
||||
${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
${positivePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 text-sm mt-1" onclick="window.admin.showFullPrompt(\'positive\')">Show more</button>' : ''}
|
||||
</div>
|
||||
|
||||
<!-- Negative prompt -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-sm font-medium text-gray-300">Negative prompt</h2>
|
||||
<button class="text-gray-400 hover:text-gray-300" onclick="window.admin.copyPrompt('negative')">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300 bg-gray-800 p-3 rounded max-h-32 overflow-y-auto">
|
||||
${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
${negativePrompt.length > 200 ? '<button class="text-blue-400 hover:text-blue-300 text-sm mt-1" onclick="window.admin.showFullPrompt(\'negative\')">Show more</button>' : ''}
|
||||
</div>
|
||||
|
||||
<!-- Other metadata -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-3">Other metadata</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">CFG SCALE: ${cfgScale}</span>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">STEPS: ${steps}</span>
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SAMPLER: ${sampler}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">SEED: ${seed}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Workflow Data -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-300 mb-2">ComfyUI Workflow</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="text-blue-400 hover:text-blue-300 text-sm" onclick="window.admin.showWorkflowData()">View Raw Workflow JSON</button>
|
||||
<button class="text-blue-400 hover:text-blue-300" onclick="window.admin.downloadWorkflowJSON()" title="Download JSON">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showMetadataError() {
|
||||
const metadataContent = document.getElementById('metadataContent');
|
||||
if (!metadataContent) return;
|
||||
|
||||
metadataContent.innerHTML = `
|
||||
<div class="text-center text-red-400 py-8">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<p class="text-lg mb-2">Error Loading Metadata</p>
|
||||
<p class="text-sm">Could not extract ComfyUI metadata from this image</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async copyPrompt(type) {
|
||||
if (!this.currentMetadata) return;
|
||||
|
||||
const text = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt;
|
||||
await this.copyToClipboard(text);
|
||||
}
|
||||
|
||||
async copyAllMetadata() {
|
||||
if (!this.currentMetadata) return;
|
||||
|
||||
const allData = `Checkpoint: ${this.currentMetadata.checkpoint || 'Unknown'}
|
||||
Positive Prompt: ${this.currentMetadata.positivePrompt}
|
||||
Negative Prompt: ${this.currentMetadata.negativePrompt}
|
||||
Steps: ${this.currentMetadata.steps || 'Unknown'}
|
||||
CFG Scale: ${this.currentMetadata.cfgScale || 'Unknown'}
|
||||
Sampler: ${this.currentMetadata.sampler || 'Unknown'}
|
||||
Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
await this.copyToClipboard(allData);
|
||||
}
|
||||
|
||||
showFullPrompt(type) {
|
||||
if (!this.currentMetadata) return;
|
||||
|
||||
const prompt = type === 'positive' ? this.currentMetadata.positivePrompt : this.currentMetadata.negativePrompt;
|
||||
const newWindow = window.open('', '_blank');
|
||||
newWindow.document.write(`
|
||||
<html>
|
||||
<head><title>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</title></head>
|
||||
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
|
||||
<h2>${type.charAt(0).toUpperCase() + type.slice(1)} Prompt</h2>
|
||||
<div style="background: #222; padding: 15px; border-radius: 5px; white-space: pre-wrap; line-height: 1.5;">${prompt}</div>
|
||||
<button onclick="navigator.clipboard.writeText(\`${prompt.replace(/`/g, '\\`')}\`).then(() => alert('Copied!'))" style="margin-top: 20px; padding: 10px 20px; background: #444; color: #fff; border: none; border-radius: 5px; cursor: pointer;">Copy to Clipboard</button>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
showWorkflowData() {
|
||||
if (!this.currentMetadata || !this.currentMetadata.workflow) return;
|
||||
|
||||
const newWindow = window.open('', '_blank');
|
||||
newWindow.document.write(`
|
||||
<html>
|
||||
<head><title>ComfyUI Workflow Data</title></head>
|
||||
<body style="background: #111; color: #fff; font-family: monospace; padding: 20px;">
|
||||
<h2>ComfyUI Workflow JSON</h2>
|
||||
<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${JSON.stringify(this.currentMetadata.workflow, null, 2)}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
downloadWorkflowJSON() {
|
||||
if (!this.currentMetadata || !this.currentMetadata.workflow) return;
|
||||
|
||||
const dataStr = JSON.stringify(this.currentMetadata.workflow, null, 2);
|
||||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'comfyui_workflow.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.showNotification('Copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
console.error('Copy failed:', err);
|
||||
this.showNotification('Copy failed', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the admin interface
|
||||
|
||||
Reference in New Issue
Block a user