From 638dd44db276e407a3eca4eefe4c501e0d9e22a8 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 11 Aug 2025 15:30:40 -0700 Subject: [PATCH] fix(gallery): complete metadata parsing improvements for all gallery views - Fixed TypeError "text.toLowerCase is not a function" by adding type checking - Added support for modern ComfyUI node types: - CFGGuider for CFG scale values - BasicScheduler for steps parameter - KSamplerSelect for sampler selection - RandomNoise/SeedHistory for seed values - Unified metadata parsing logic between main gallery and metadata viewer - Fixed prompt identification to properly distinguish positive vs negative prompts - Added NaN cleaning for JSON parsing to handle malformed data - Improved text node analysis with PromptManager priority All three gallery implementations now correctly parse and display: - Checkpoint names - Positive and negative prompts - Technical parameters (steps, CFG, sampler, seed) - Support for both legacy and modern ComfyUI workflows Tested and verified with test_metadata_file.png across all implementations. --- web/gallery.html | 23 +++-- web/metadata.html | 218 ++++++++++++++++++++++++++-------------------- 2 files changed, 140 insertions(+), 101 deletions(-) diff --git a/web/gallery.html b/web/gallery.html index 00a4c61..8e2f1dc 100644 --- a/web/gallery.html +++ b/web/gallery.html @@ -2042,6 +2042,10 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // if we haven't found them from PromptManager for (const textNode of textNodes) { const text = textNode.text; + // Ensure text is a string before calling toLowerCase + if (!text || typeof text !== 'string') { + continue; + } const textLower = text.toLowerCase(); // Skip if this is the PromptManager text we already have @@ -2091,14 +2095,17 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; // CLIPTextEncode nodes if (node.type === 'CLIPTextEncode' && node.widgets_values && node.widgets_values[0]) { const text = node.widgets_values[0]; - const textLower = text.toLowerCase(); - - if (textLower.includes('bad anatomy') || textLower.includes('unfinished') || - textLower.includes('censored') || textLower.includes('negative') || - textLower.includes('worst quality') || textLower.includes('low quality')) { - negativePrompt = text; - } else if (positivePrompt === 'No prompt found') { - positivePrompt = text; + // Ensure text is a string before calling toLowerCase + if (text && typeof text === 'string') { + const textLower = text.toLowerCase(); + + if (textLower.includes('bad anatomy') || textLower.includes('unfinished') || + textLower.includes('censored') || textLower.includes('negative') || + textLower.includes('worst quality') || textLower.includes('low quality')) { + negativePrompt = text; + } else if (positivePrompt === 'No prompt found') { + positivePrompt = text; + } } } diff --git a/web/metadata.html b/web/metadata.html index e4a61e8..30ef2c9 100644 --- a/web/metadata.html +++ b/web/metadata.html @@ -164,10 +164,15 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`; for (const field of workflowFields) { if (metadata[field]) { try { - workflowData = JSON.parse(metadata[field]); + // 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); break; } catch (e) { - console.log('Failed to parse workflow field:', field); + console.log('Failed to parse workflow field:', field, e); } } } @@ -175,10 +180,15 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`; for (const field of promptFields) { if (metadata[field]) { try { - promptData = JSON.parse(metadata[field]); + // 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); break; } catch (e) { - console.log('Failed to parse prompt field:', field); + console.log('Failed to parse prompt field:', field, e); } } } @@ -204,34 +214,50 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`; console.log('Parsing prompt data...'); const promptNodes = comfyData.prompt; + // Track all text nodes and try to identify positive/negative + const textNodes = []; + 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; + // Checkpoint - check multiple types + if ((node.class_type === 'CheckpointLoaderSimple' || + node.class_type === 'UNETLoader' || + node.class_type === 'DualCLIPLoader') && node.inputs) { + checkpoint = node.inputs.ckpt_name || node.inputs.unet_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)); + // Collect all text nodes for analysis + if (node.inputs && node.inputs.text) { + textNodes.push({ + nodeId: nodeId, + classType: node.class_type, + text: node.inputs.text, + inputs: node.inputs + }); } - 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 + // Special handling for PromptManager nodes + if (node.class_type === 'PromptManager' && node.inputs) { + // PromptManager always contains the positive prompt + if (node.inputs.text) { positivePrompt = node.inputs.text; - console.log('Found potential positive prompt:', positivePrompt.substring(0, 100)); + console.log('Found PromptManager with text:', positivePrompt.substring(0, 100)); + } + // Also check for loaded prompt + if (node.inputs.selected_prompt) { + positivePrompt = node.inputs.selected_prompt; + } + } + + // PromptManagerText node + if (node.class_type === 'PromptManagerText' && node.inputs) { + if (node.inputs.text) { + positivePrompt = node.inputs.text; + } + if (node.inputs.selected_prompt) { + positivePrompt = node.inputs.selected_prompt; } } @@ -240,10 +266,10 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`; node.class_type === 'KSamplerAdvanced' || node.class_type === 'SamplerCustom' || node.class_type === 'SamplerCustomAdvanced') && node.inputs) { - seed = node.inputs.seed || seed; steps = node.inputs.steps || steps; cfgScale = node.inputs.cfg || cfgScale; sampler = node.inputs.sampler_name || sampler; + seed = node.inputs.seed || node.inputs.noise_seed || seed; } // New ComfyUI node types for modern workflows @@ -268,83 +294,89 @@ Seed: ${currentWorkflowData.seed || 'Unknown'}`; } } } - } - - // 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; + // Process collected text nodes to identify positive/negative prompts + // if we haven't found them from PromptManager + for (const textNode of textNodes) { + const text = textNode.text; + // Ensure text is a string before calling toLowerCase + if (!text || typeof text !== 'string') { + continue; + } + const textLower = text.toLowerCase(); + + // Skip if this is the PromptManager text we already have + if (textNode.classType === 'PromptManager' || textNode.classType === 'PromptManagerText') { + continue; + } + + // Identify negative prompts by common patterns + if (textNode.classType === 'CLIPTextEncode') { + if (textLower.includes('bad anatomy') || textLower.includes('unfinished') || + textLower.includes('censored') || textLower.includes('weird anatomy') || + textLower.includes('negative') || textLower.includes('embedding:') || + textLower.includes('worst quality') || textLower.includes('low quality')) { + negativePrompt = 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 = text; + console.log('Found potential positive prompt:', positivePrompt.substring(0, 100)); } } } + } - // Look for sampler nodes - check multiple types - const samplerNode = nodes.find(node => - node.type === 'KSampler' || - node.type === 'KSamplerAdvanced' || - node.type === 'SamplerCustom' || - node.type === 'SamplerCustomAdvanced' - ); - 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; - } - - // Look for newer node types - if (steps === 'Unknown') { - const schedulerNode = nodes.find(node => node.type === 'BasicScheduler'); - if (schedulerNode && schedulerNode.widgets_values) { - steps = schedulerNode.widgets_values[1]; // steps is usually second parameter + // Also try to parse from workflow data if we didn't find everything in prompt + if (comfyData.workflow && comfyData.workflow.nodes) { + for (const node of comfyData.workflow.nodes) { + // Checkpoint loaders + if ((node.type === 'CheckpointLoaderSimple' || + node.type === 'UNETLoader' || + node.type === 'DualCLIPLoader') && node.widgets_values) { + checkpoint = node.widgets_values[0] || checkpoint; } - } - - if (cfgScale === 'Unknown') { - const cfgNode = nodes.find(node => node.type === 'CFGGuider'); - if (cfgNode && cfgNode.widgets_values) { - cfgScale = cfgNode.widgets_values[0]; // cfg is usually first parameter + + // PromptManager nodes in workflow + if (node.type === 'PromptManager' && node.widgets_values) { + // The first widget value is usually the text + if (node.widgets_values[0] && positivePrompt === 'No prompt found') { + positivePrompt = node.widgets_values[0]; + } } - } - - if (sampler === 'Unknown') { - const samplerSelectNode = nodes.find(node => node.type === 'KSamplerSelect'); - if (samplerSelectNode && samplerSelectNode.widgets_values) { - sampler = samplerSelectNode.widgets_values[0]; // sampler name is usually first parameter + + if (node.type === 'PromptManagerText' && node.widgets_values) { + if (node.widgets_values[0] && positivePrompt === 'No prompt found') { + positivePrompt = node.widgets_values[0]; + } } - } - - if (seed === 'Unknown') { - const noiseNode = nodes.find(node => node.type === 'RandomNoise'); - if (noiseNode && noiseNode.widgets_values) { - seed = noiseNode.widgets_values[0]; // seed is usually first parameter + + // CLIPTextEncode nodes + if (node.type === 'CLIPTextEncode' && node.widgets_values && node.widgets_values[0]) { + const text = node.widgets_values[0]; + // Ensure text is a string before calling toLowerCase + if (text && typeof text === 'string') { + const textLower = text.toLowerCase(); + + if (textLower.includes('bad anatomy') || textLower.includes('unfinished') || + textLower.includes('censored') || textLower.includes('negative') || + textLower.includes('worst quality') || textLower.includes('low quality')) { + negativePrompt = text; + } else if (positivePrompt === 'No prompt found') { + positivePrompt = text; + } + } + } + + // Samplers + if ((node.type === 'KSampler' || node.type === 'KSamplerAdvanced' || + node.type === 'SamplerCustom' || node.type === 'SamplerCustomAdvanced') && node.widgets_values) { + if (node.widgets_values.length >= 4) { + seed = node.widgets_values[0] || seed; + steps = node.widgets_values[1] || steps; + cfgScale = node.widgets_values[2] || cfgScale; + sampler = node.widgets_values[3] || sampler; + } } } }