- Fit to Window
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fit to Window
+
+
+
+
+
+
+
+
![Generated image]()
+
+
+
+
+ 1 / 1
+
-
-
-
-
![Generated image]()
-
-
-
-
@@ -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
+
+ ${filePath}
+
+
+
+
+
+
Resources used
+
+
+
${checkpoint}
+
ComfyUI Generated
+
+
CHECKPOINT
+
+
+
+
+
+
+
Prompt
+
COMFYUI
+
+
+
+ ${positivePrompt.substring(0, 200)}${positivePrompt.length > 200 ? '...' : ''}
+
+ ${positivePrompt.length > 200 ? '
' : ''}
+
+
+
+
+
+
Negative prompt
+
+
+
+ ${negativePrompt.substring(0, 200)}${negativePrompt.length > 200 ? '...' : ''}
+
+ ${negativePrompt.length > 200 ? '
' : ''}
+
+
+
+
+
Other metadata
+
+ CFG SCALE: ${cfgScale}
+ STEPS: ${steps}
+ SAMPLER: ${sampler}
+
+
+ SEED: ${seed}
+
+
+
+
+
+
ComfyUI Workflow
+
+
+
+
+
+ `;
+ }
+
+ showMetadataError() {
+ const metadataContent = document.getElementById('metadataContent');
+ if (!metadataContent) return;
+
+ metadataContent.innerHTML = `
+
+
+
Error Loading Metadata
+
Could not extract ComfyUI metadata from this image
+
+ `;
+ }
+
+ 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(`
+
+
${type.charAt(0).toUpperCase() + type.slice(1)} Prompt
+
+
${type.charAt(0).toUpperCase() + type.slice(1)} Prompt
+
${prompt}
+
+
+
+ `);
+ }
+
+ showWorkflowData() {
+ if (!this.currentMetadata || !this.currentMetadata.workflow) return;
+
+ const newWindow = window.open('', '_blank');
+ newWindow.document.write(`
+
+
ComfyUI Workflow Data
+
+
ComfyUI Workflow JSON
+
${JSON.stringify(this.currentMetadata.workflow, null, 2)}
+
+
+ `);
+ }
+
+ 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