diff --git a/AGENTS.md b/AGENTS.md index b49afdc..b7b825b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,9 +85,6 @@ dev-docs/ ├── README.md # Developer documentation index │ ├── nodes/ # Node-specific documentation (ONE file per node) -│ ├── video-metadata/ # Video Metadata node -│ │ ├── README.md # Quick reference -│ │ └── VIDEO_METADATA.md # Complete documentation │ ├── lm-studio-describe/ # LM Studio structured describe nodes │ ├── reddit-media/ # Reddit Media Extraction │ ├── media-selection/ # Media Selection node @@ -252,17 +249,17 @@ python3 -c "from nodes.nodes import NODE_CLASS_MAPPINGS; print('Available nodes: ```bash # Example workflow for modifying a node -vim nodes/video_metadata/video_metadata.py # Edit node implementation +vim nodes/media_selection/media_selection.py # Edit node implementation # Update technical documentation -vim dev-docs/nodes/video-metadata/VIDEO_METADATA.md +vim dev-docs/nodes/media-selection/MEDIA_SELECTION.md # Update user help page -vim web/docs/VideoMetadata.md +vim web/docs/MediaSelection.md # Commit both together -git add nodes/video_metadata/ dev-docs/nodes/video-metadata/ web/docs/VideoMetadata.md -git commit -m "feat: Add duration_threshold parameter to VideoMetadata +git add nodes/media_selection/ dev-docs/nodes/media-selection/ web/docs/MediaSelection.md +git commit -m "feat: Add duration_threshold parameter to MediaSelection - Added duration_threshold parameter - Updated technical docs with implementation details @@ -275,9 +272,6 @@ git commit -m "feat: Add duration_threshold parameter to VideoMetadata dev-docs/ ├── README.md # Developer documentation index ├── nodes/ # Node-specific documentation (ONE file per node) -│ ├── video-metadata/ -│ │ ├── README.md # Quick reference -│ │ └── VIDEO_METADATA.md # Complete consolidated documentation │ ├── lm-studio-describe/ # LM Studio structured describe docs │ ├── reddit-media/ # Reddit Media Extraction docs │ ├── media-selection/ # Media Selection node docs @@ -324,7 +318,7 @@ touch dev-docs/integrations/[service]/NEW_INTEGRATION.md **Developer docs (`dev-docs/`):** -- **Nodes**: Use descriptive all-caps names: `VIDEO_METADATA.md`, `MEDIA_DESCRIBE.md` +- **Nodes**: Use descriptive all-caps names: `MEDIA_SELECTION.md`, `MEDIA_DESCRIBE.md` - **ONE file per node**: Consolidate all information about a node into a single comprehensive file - Node docs go in `dev-docs/nodes/[node-name]/` - Infrastructure docs go in `dev-docs/infrastructure/[category]/` @@ -333,7 +327,7 @@ touch dev-docs/integrations/[service]/NEW_INTEGRATION.md **User help pages (`web/docs/`):** - **MUST match** `NODE_CLASS_MAPPINGS` key exactly (case-sensitive) -- Example: `NODE_CLASS_MAPPINGS = {"VideoMetadata": ...}` → `web/docs/VideoMetadata.md` +- Example: `NODE_CLASS_MAPPINGS = {"MediaSelection": ...}` → `web/docs/MediaSelection.md` **CRITICAL**: Whenever you implement a new feature, fix a bug, or solve a technical problem: @@ -837,7 +831,7 @@ else: " # Example output: -# ✅ Found: web/docs/VideoMetadata.md +# ✅ Found: web/docs/MediaSelection.md # ❌ Missing: web/docs/GeminiUtilVideoDescribe.md ``` @@ -954,8 +948,8 @@ ls -la dev-docs/examples/ # Example workflows ls -la web/docs/ # User help pages (MANDATORY for all nodes) # View node documentation -cat dev-docs/nodes/video-metadata/VIDEO_METADATA.md # Complete technical docs -cat web/docs/VideoMetadata.md # User help page +cat dev-docs/nodes/media-selection/MEDIA_SELECTION.md # Complete technical docs +cat web/docs/MediaSelection.md # User help page # Common documentation patterns: # Developer docs (dev-docs/): @@ -981,9 +975,6 @@ comfyui_swissarmyknife/ ├── dev-docs/ # DEVELOPER DOCUMENTATION (Technical) │ ├── README.md # Developer documentation index │ ├── nodes/ # Node-specific documentation (ONE file per node) -│ │ ├── video-metadata/ # Video Metadata node docs -│ │ │ ├── README.md # Quick reference -│ │ │ └── VIDEO_METADATA.md # Complete documentation │ │ ├── lm-studio-describe/ # LM Studio structured describe docs │ │ ├── reddit-media/ # Reddit Media Extraction docs │ │ ├── media-selection/ # Media Selection node docs @@ -1009,8 +1000,8 @@ comfyui_swissarmyknife/ │ │ └── ... │ ├── css/ # Stylesheets │ └── docs/ # USER HELP PAGES (In-UI reference) -│ ├── VideoMetadata.md # Help page for VideoMetadata node -│ ├── ControlPanelOverview.md # Help page for ControlPanelOverview node +│ ├── MediaSelection.md # Help page for MediaSelection node +│ ├── ControlPanelPromptBreakdown.md # Help page for Control Panel output node │ └── ... # (MUST match NODE_CLASS_MAPPINGS keys) │ └── ui-react_backup/ # React UI extension (DISABLED) diff --git a/README.md b/README.md index fb86dba..e168b1c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The following nodes have been removed as they are now redundant or better served - **FilenameGenerator** - Use ComfyUI's built-in string nodes - **VideoMetadataNode** - Functionality merged into enhanced video preview - **ShowText** - Use ComfyUI's built-in display nodes +- **ControlPanelOverview** - Use Control Panel Prompt Breakdown or built-in viewers - **FrameExtractor** - Use ComfyUI's built-in video frame extraction - **MultiCaptionCombiner** - Use ComfyUI's built-in string concatenation - **GeminiUtilOptions** - Consolidated into main Gemini nodes diff --git a/dev-docs/CONSOLIDATION_GUIDE.md b/dev-docs/CONSOLIDATION_GUIDE.md index 3952a69..bbe800c 100644 --- a/dev-docs/CONSOLIDATION_GUIDE.md +++ b/dev-docs/CONSOLIDATION_GUIDE.md @@ -9,12 +9,9 @@ This guide documents the documentation consolidation process for ComfyUI-SwissAr ## Consolidation Status -### ✅ Completed Nodes (1/6) +### ✅ Completed / Retired Nodes -1. **video-metadata** (5 files → 1 file) - - Consolidated into: `VIDEO_METADATA.md` - - Includes: Append functionality, VHS compatibility, LoRA JSON integration, troubleshooting - - Size: ~14.8 KB +1. **video-metadata** – Node retired (November 30, 2025). Documentation set removed because the FFmpeg metadata helper is no longer shipped. > **Note:** The legacy `video-preview` node has been retired, so its documentation set was removed rather than consolidated. diff --git a/dev-docs/README.md b/dev-docs/README.md index 1b8f898..8515b69 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -25,7 +25,7 @@ Welcome to the ComfyUI-SwissArmyKnife **developer documentation**! Core custom nodes that extend ComfyUI functionality: -- **[video-metadata](nodes/video-metadata/)** - Video metadata extraction and manipulation with append functionality +- ~~**video-metadata**~~ (retired November 30, 2025) - FFmpeg metadata helper removed from the repo - **[lm-studio-describe](nodes/lm-studio-describe/)** - Structured LM Studio describe nodes with JSON outputs - **[reddit-media](nodes/reddit-media/)** - Reddit and RedGifs media extraction and processing - **[media-selection](nodes/media-selection/)** - Media source selection and management @@ -105,7 +105,7 @@ Each node should have a single comprehensive markdown file that includes: ### By Topic -- **Video Processing**: Check `/nodes/video-metadata/` +- **Video Processing**: See `/nodes/media-selection/` and `/nodes/reddit-media/` - **AI/ML Features**: See `/nodes/lm-studio-describe/` - **External APIs**: Look in `/integrations/` - **Performance & Monitoring**: Check `/web-js/RESOURCE_MONITOR.md` for profiling and OOM analysis diff --git a/dev-docs/examples/WORKFLOW_DEMO.md b/dev-docs/examples/WORKFLOW_DEMO.md index 92c8ad7..c437e3a 100644 --- a/dev-docs/examples/WORKFLOW_DEMO.md +++ b/dev-docs/examples/WORKFLOW_DEMO.md @@ -3,20 +3,25 @@ This shows how to set up a workflow to test the final_string widget functionality: ``` -┌─────────────┐ ┌─────────────────────────┐ ┌─────────────┐ -│ Load Image │ │ Gemini Image Describe │ │ Show Text │ -│ │────▶│ │────▶│ │ -│ [Load your │ │ - API Key: [your key] │ │ Displays │ -│ test image]│ │ - Model: gemini-2.5- │ │ final │ -│ │ │ flash │ │ generated │ -└─────────────┘ │ - System Prompt: [...] │ │ prompt │ - │ - User Prompt: [...] │ └─────────────┘ +┌─────────────┐ ┌─────────────────────────┐ +│ Load Image │ │ Gemini Image Describe │ +│ │────▶│ │ +│ [Load your │ │ - API Key: [your key] │ +│ test image]│ │ - Model: gemini-2.5- │ +│ │ │ flash │ +└─────────────┘ │ - System Prompt: [...] │ + │ - User Prompt: [...] │ │ │ │ ┌─────────────────────┐ │ - │ │ final_string widget │ │ ← This widget will - │ │ (will auto-update) │ │ populate after + │ │ final_string widget │ │ ← This widget auto- + │ │ (will auto-update) │ │ populates after │ └─────────────────────┘ │ execution! └─────────────────────────┘ + │ + ▼ + (Optional) connect `final_string` + to any built-in ComfyUI text display node + if you want it rendered outside the Gemini node. ``` ### Step-by-step Instructions: @@ -28,7 +33,7 @@ This shows how to set up a workflow to test the final_string widget functionalit 2. **Set up the nodes**: - Load Image: Choose any test image - Gemini Image Describe: Enter your valid Gemini API key - - Show Text: This will display the final output + - (Optional) add a built-in ComfyUI text output node (e.g., `Text`/`Note`) and wire it to `final_string` if you want the caption outside of the Gemini panel 3. **Execute the workflow**: - Click "Queue Prompt" to run the workflow @@ -36,7 +41,7 @@ This shows how to set up a workflow to test the final_string widget functionalit 4. **Check the results**: - The `final_string` widget on the Gemini node should populate with generated text - - The Show Text node should display the same text + - Optional downstream text nodes should mirror the same string - Browser console should show "Updated final_string widget with:" message ### What was fixed: diff --git a/dev-docs/examples/example_workflow.json b/dev-docs/examples/example_workflow.json index cdc0b38..3fd7b7e 100644 --- a/dev-docs/examples/example_workflow.json +++ b/dev-docs/examples/example_workflow.json @@ -1,6 +1,6 @@ { - "last_node_id": 4, - "last_link_id": 3, + "last_node_id": 2, + "last_link_id": 1, "nodes": [ { "id": 1, @@ -48,7 +48,7 @@ { "name": "final_string", "type": "STRING", - "links": [2], + "links": null, "shape": 3, "slot_index": 2 } @@ -61,26 +61,10 @@ "Please analyze this image...", "" ] - }, - { - "id": 3, - "type": "ShowText", - "pos": [1000, 100], - "size": { "0": 300, "1": 200 }, - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [{ "name": "text", "type": "STRING", "link": 2 }], - "outputs": [ - { "name": "text", "type": "STRING", "links": null, "shape": 3 } - ], - "properties": { "Node name for S&R": "ShowText" }, - "widgets_values": [""] } ], "links": [ - [1, 1, 0, 2, 0, "IMAGE"], - [2, 2, 2, 3, 0, "STRING"] + [1, 1, 0, 2, 0, "IMAGE"] ], "groups": [], "config": {}, diff --git a/dev-docs/nodes/control-panel/CONTROL_PANEL.md b/dev-docs/nodes/control-panel/CONTROL_PANEL.md index 8192eb8..36201f3 100644 --- a/dev-docs/nodes/control-panel/CONTROL_PANEL.md +++ b/dev-docs/nodes/control-panel/CONTROL_PANEL.md @@ -1,691 +1,65 @@ -# Control Panel Data Population Debugging Enhancement - -## Issue - -The Control Panel node was not displaying data from `all_media_describe_data` input, even though the data was confirmed to exist in the workflow. - -## Solution Implemented - -### 1. Backend (Python) Enhancements - -**File**: `nodes/utils/control_panel.py` - -#### Changes: - -1. **Doubled output data** - Return both `all_media_describe_data` and `all_media_describe_data_copy` in the UI dict for redundancy -2. **Added comprehensive logging** - Print all kwargs received to debug what data is coming in -3. **Added output logging** - Print the full result structure being returned to the UI -4. **Debug-controlled logging** - All Python console output now uses `nodes/debug_utils.Logger`, so it only appears when the global debug flag (environment `DEBUG=true` or the Control Panel debug toggle) is enabled - -```python -# DOUBLE the output - return BOTH in ui dict -result = { - "ui": { - "all_media_describe_data": [all_media_data], - "all_media_describe_data_copy": [all_media_data] # Duplicate for redundancy - } -} -``` - -#### Debug Output: - -``` -🔍 DEBUG - All kwargs received: - - all_media_describe_data: str = {"final_prompt": "...", ...} - -🔍 DEBUG - Returning to UI: -{ - "ui": { - "all_media_describe_data": [...], - "all_media_describe_data_copy": [...] - } -} -``` - -### 2. Frontend (JavaScript) Enhancements - -**File**: `web/js/swiss-army-knife.js` - -#### Changes in `updateControlPanelData`: - -1. **Console logging always enabled** - Using `console.log` instead of `debugLog` for critical debugging -2. **Try multiple data sources** - Check both `data.all_media_describe_data` and `data.all_media_describe_data_copy` -3. **Comprehensive logging** - Log every step of data extraction and parsing -4. **Log all object keys** - Show what keys are available at each level - -```javascript -// Try all possible locations for the data -if (data.all_media_describe_data) { - console.log('🔍 [ControlPanel DEBUG] Found data.all_media_describe_data'); - rawData = Array.isArray(data.all_media_describe_data) - ? data.all_media_describe_data[0] - : data.all_media_describe_data; -} else if (data.all_media_describe_data_copy) { - console.log( - '🔍 [ControlPanel DEBUG] Found data.all_media_describe_data_copy', - ); - rawData = Array.isArray(data.all_media_describe_data_copy) - ? data.all_media_describe_data_copy[0] - : data.all_media_describe_data_copy; -} -``` - -#### Changes in `onExecuted`: - -1. **Full message structure logging** - Log the complete message object received -2. **Log all keys at each level** - Show what properties are available -3. **Trace execution path** - Show which code paths are being taken - -```javascript -console.log('🔍 [ControlPanel DEBUG] onExecuted called'); -console.log('🔍 [ControlPanel DEBUG] Full message object:', message); -console.log( - '🔍 [ControlPanel DEBUG] message keys:', - Object.keys(message || {}), -); -``` - -## Debugging Workflow - -### Step 1: Check Python Console Output - -Make sure debug mode is enabled (`DEBUG=true` or toggle in the Control Panel settings); only then will the Control Panel logs appear in the ComfyUI server console because they now use the centralized debug logger. - -Look for the Control Panel debug output in the ComfyUI server logs: - -``` -============================================================ -CONTROL PANEL - Workflow Information -============================================================ - -🔍 DEBUG - All kwargs received: - - all_media_describe_data: str = {...} - -📊 Analysis JSON Input: -{ - "final_prompt": "...", - "gemini_status": "...", - "media_info": "..." -} - -🔍 DEBUG - Returning to UI: -{...} -``` - -### Step 2: Check Browser Console Output - -Open browser DevTools and look for the Control Panel debug messages: - -``` -🔍 [ControlPanel DEBUG] onExecuted called -🔍 [ControlPanel DEBUG] Full message object: {...} -🔍 [ControlPanel DEBUG] message keys: ["output"] -🔍 [ControlPanel DEBUG] message.output: {...} -🔍 [ControlPanel DEBUG] message.output keys: ["all_media_describe_data", "all_media_describe_data_copy"] -🔍 [ControlPanel DEBUG] updateControlPanelData called -🔍 [ControlPanel DEBUG] data received: {...} -🔍 [ControlPanel DEBUG] Found data.all_media_describe_data -🔍 [ControlPanel DEBUG] rawData: "{...}" -🔍 [ControlPanel DEBUG] Parsed mediaData: {...} -🔍 [ControlPanel DEBUG] mediaData keys: ["final_prompt", "gemini_status", ...] -``` - -### Step 3: Identify the Issue - -Based on the debug output, you can identify: - -- ✅ Is data being sent from Python? (Check Python console) -- ✅ Is data arriving in JavaScript? (Check `onExecuted` logs) -- ✅ Which data structure is being used? (Check `message.output` keys) -- ✅ Is JSON parsing successful? (Check parsed mediaData) -- ✅ Which fields are available? (Check mediaData keys) - -## Common Issues and Solutions - -### Issue 1: Data not arriving in JavaScript - -**Symptom**: Python shows data being returned, but JavaScript shows empty message.output - -**Solution**: Check that the node is properly registered as an OUTPUT_NODE in Python - -### Issue 2: Data structure mismatch - -**Symptom**: JavaScript can't find `all_media_describe_data` in the message - -**Solution**: The double-output approach now checks both `all_media_describe_data` and `all_media_describe_data_copy` - -### Issue 3: JSON parsing fails - -**Symptom**: Error in browser console about JSON parsing - -**Solution**: Check that the data is properly JSON serialized in Python before sending - -## Testing - -### Manual Test Steps: - -1. Start ComfyUI server with the custom node installed -2. Create a workflow with: - - A media source node (e.g., Load Video) - - LM Studio Structured Describe nodes - - Control Panel node connected to `all_media_describe_data` output -3. Execute the workflow -4. Check both Python console and browser console for debug output -5. Verify Control Panel displays the data in three columns - -### Expected Output: - -- **Left Column**: Final prompt text -- **Middle Column**: Gemini status information -- **Right Column**: Media info (height, width, description, etc.) - -## Performance Considerations - -The debug logging uses `console.log` instead of `debugLog`, which means it will always appear in the console even when DEBUG mode is disabled. This is intentional for critical debugging but should be considered for production. - -**Future Enhancement**: Consider adding a separate `CONTROL_PANEL_DEBUG` flag that can be toggled independently. - -## Related Files - -- `nodes/utils/control_panel.py` - Backend node implementation -- `web/js/swiss-army-knife.js` - Frontend widget implementation (lines 380-520) -- `docs/nodes/control-panel/CONTROL_PANEL_DATA_POPULATION_DEBUG.md` - This document - -## Implementation Date - -October 11, 2025 - -## Status - -✅ Implemented - Comprehensive debugging in place for both backend and frontend - ---- - -# Control Panel Data Population Fix - -## Issue Summary - -The Control Panel node was displaying "(No final_prompt in data)" even though the `all_media_describe_data` was successfully being passed to the node. - -## Root Cause Analysis - -After adding comprehensive debugging, the issues were identified: - -### 1. Field Name Mismatch ✅ FIXED - -**Problem**: Code was checking for `mediaData.final_prompt` but the actual field name in the data is `final_string`. - -**Evidence from debug logs**: - -```javascript -// mediaData keys showed: -['description', 'media_info', 'gemini_status', 'final_string', 'height', 'width', - 'subject', 'cinematic_aesthetic', 'stylization_tone', 'clothing', 'scene', 'movement'] - -// But code was checking: -if (mediaData.final_prompt) { ... } // ❌ This field doesn't exist! -``` - -**Fix**: Updated to check multiple possible field names in priority order: - -```javascript -const finalText = - mediaData.final_string || mediaData.final_prompt || mediaData.description; -``` - -### 2. Message Structure Handling ✅ FIXED - -**Problem**: Code assumed data would be in `message.output`, but ComfyUI was sending it at the root level of the message. - -**Evidence from debug logs**: - -```javascript -// What we received: -message = { - all_media_describe_data: [...], - all_media_describe_data_copy: [...] -} - -// What the code was looking for: -message.output.all_media_describe_data // ❌ message.output was undefined -``` - -**Fix**: Updated `onExecuted` handler to check both locations: - -```javascript -if (message && message.output) { - this.updateControlPanelData(message.output); -} else if ( - message && - (message.all_media_describe_data || message.all_media_describe_data_copy) -) { - this.updateControlPanelData(message); // Handle root-level data -} -``` - -## Changes Made - -### File: `web/js/swiss-army-knife.js` - -#### Change 1: Flexible Field Name Checking - -**Location**: Lines ~462-471 (updateControlPanelData function) - -**Before**: - -```javascript -// Left column: Final Prompt -if (mediaData.final_prompt) { - this._cp_leftColumn.textContent = formatValue(mediaData.final_prompt, 2000); -} else { - this._cp_leftColumn.textContent = '(No final_prompt in data)'; -} -``` - -**After**: - -```javascript -// Left column: Final String/Prompt (check both field names) -const finalText = - mediaData.final_string || mediaData.final_prompt || mediaData.description; -if (finalText) { - console.log( - '🔍 [ControlPanel DEBUG] Setting final text:', - finalText.substring(0, 50), - ); - this._cp_leftColumn.textContent = formatValue(finalText, 2000); -} else { - console.log( - '🔍 [ControlPanel DEBUG] No final_string/final_prompt/description in mediaData', - ); - this._cp_leftColumn.textContent = '(No final text in data)'; -} -``` - -#### Change 2: Handle Multiple Message Structures - -**Location**: Lines ~554-567 (onExecuted handler) - -**Before**: - -```javascript -if (message && message.output) { - this.updateControlPanelData(message.output); -} else { - console.log('🔍 [ControlPanel DEBUG] No message.output available'); -} -``` - -**After**: - -```javascript -// Try multiple data sources - data might be at root or in output -if (message && message.output) { - console.log( - '🔍 [ControlPanel DEBUG] Calling updateControlPanelData with message.output:', - message.output, - ); - this.updateControlPanelData(message.output); -} else if ( - message && - (message.all_media_describe_data || message.all_media_describe_data_copy) -) { - console.log( - '🔍 [ControlPanel DEBUG] Calling updateControlPanelData with message (root level):', - message, - ); - this.updateControlPanelData(message); -} else { - console.log( - '🔍 [ControlPanel DEBUG] No data found in message.output or message root', - ); -} -``` - -#### Change 3: Updated Displayed Keys List - -**Location**: Lines ~505-513 - -**Before**: - -```javascript -const displayedKeys = [ - 'final_prompt', - 'gemini_status', - 'media_info', - 'height', - 'width', -]; -``` - -**After**: - -```javascript -const displayedKeys = [ - 'final_prompt', - 'final_string', - 'description', - 'gemini_status', - 'media_info', - 'height', - 'width', -]; -``` - -## Testing Results - -### Before Fix - -``` -Left Column: "(No final_prompt in data)" -Middle Column: "🤖 Gemini Analysis Status: ✅ Complete..." -Right Column: [All other fields listed including subject, cinematic_aesthetic, etc.] -``` - -### After Initial Fix - -``` -Left Column: "A woman with straight, flowing blonde hair stands upright..." -Middle Column: "🤖 Gemini Analysis Status: ✅ Complete..." -Right Column: [Media info, height, width, subject, cinematic_aesthetic, stylization_tone, clothing, scene, movement] -``` - -### After Cleanup (October 11, 2025 - Second Update) - -Removed verbose fields from right column to show only essential metadata: - -``` -Left Column: "A woman with straight, flowing blonde hair stands upright..." -Middle Column: "🤖 Gemini Analysis Status: ✅ Complete..." -Right Column: [Media info, height, width ONLY] -``` - -**Hidden fields**: subject, cinematic_aesthetic, stylization_tone, clothing, scene, movement -**Rationale**: These verbose description fields are already shown in the left column (final_string). Displaying them again in the right column created unnecessary clutter. - -## Debug Logs Analysis - -The comprehensive debug logging revealed: - -1. ✅ Data **IS** being sent from Python backend -2. ✅ Data **IS** arriving in JavaScript -3. ✅ JSON parsing **IS** successful -4. ❌ Field name mismatch prevented display (NOW FIXED) -5. ❌ Message structure assumption was incorrect (NOW FIXED) - -### Key Debug Output - -``` -🔍 [ControlPanel DEBUG] onExecuted called -🔍 [ControlPanel DEBUG] message keys: ['all_media_describe_data', 'all_media_describe_data_copy'] -🔍 [ControlPanel DEBUG] message.output: undefined ← This showed the structure issue -🔍 [ControlPanel DEBUG] Found data.all_media_describe_data -🔍 [ControlPanel DEBUG] Parsed mediaData: {...} -🔍 [ControlPanel DEBUG] mediaData keys: ['description', 'media_info', 'gemini_status', 'final_string', ...] -🔍 [ControlPanel DEBUG] No final_prompt in mediaData ← This showed the field name issue -``` - -## Lessons Learned - -### 1. Field Naming Consistency - -Different parts of the workflow use different field names: - -- `final_string` - Caption/summary output from upstream describe nodes -- `final_prompt` - Expected by Control Panel (legacy name?) -- `description` - Fallback field - -**Solution**: Use flexible fallback checking for multiple field names. - -### 2. ComfyUI Message Structure Variability - -ComfyUI can send node execution results in different structures: - -- `message.output.field_name` - Standard OUTPUT_NODE pattern -- `message.field_name` - Direct root-level data - -**Solution**: Check both locations when handling `onExecuted` messages. - -### 3. Importance of Comprehensive Debugging - -The initial approach of doubling the output (both `all_media_describe_data` and `all_media_describe_data_copy`) helped confirm data was arriving, but the extensive console logging was crucial to identifying the actual issues. - -### 4. Display Clutter Reduction (October 11, 2025) - -After fixing the initial issues, the right column was displaying many verbose fields (subject, cinematic_aesthetic, stylization_tone, clothing, scene, movement) that duplicated content already shown in the left column's final_string field. - -**Solution**: Removed the loop that added "all other fields" to only display essential metadata (media_info, height, width). - -## Next Steps - -### Short Term - -- [x] Fix field name mismatch -- [x] Handle both message structures -- [x] Test with real workflow -- [x] Remove verbose duplicate fields from right column -- [ ] Monitor for any edge cases - -### Long Term - -- [ ] Standardize field naming across all nodes -- [ ] Document ComfyUI message structure patterns -- [ ] Consider adding field name mapping configuration -- [ ] Add visual indicator when using fallback field names -- [ ] Make displayed fields configurable via node settings - -## Related Files - -- `web/js/swiss-army-knife.js` - Frontend implementation (Control Panel widget) -- `nodes/utils/control_panel.py` - Backend implementation -- `docs/nodes/control-panel/CONTROL_PANEL_DATA_POPULATION_DEBUG.md` - Original debug documentation -- `docs/nodes/control-panel/README.md` - Control Panel overview - -## Implementation Date - -October 11, 2025 - -## Status - -✅ FIXED - Both field name mismatch and message structure issues resolved - ---- - -# Control Panel Prompt Breakdown Implementation - -## Overview - -The Control Panel system consists of two nodes that display workflow information in different formats: - -1. **ControlPanelOverview** - Displays full workflow data in 3 columns -2. **ControlPanelPromptBreakdown** - Displays parsed prompt data in 6 columns - -## ControlPanelPromptBreakdown Node - -### Purpose - -Breaks down a long-form description (from Gemini AI video analysis) into structured categories for easier reading and prompt engineering. - -### Input - -- `all_media_describe_data` (STRING) - JSON data containing the full media analysis, including a `description` field - -### Output - -Displays parsed description text across 6 columns: - -1. **Subject** - First paragraph (character/subject description) -2. **Cinematic/Aesthetic** - Second paragraph (camera work, lighting, framing) -3. **Stylization/Tone** - Third paragraph (overall mood, style, aesthetic) -4. **Clothing** - Fourth paragraph (wardrobe, attire details) -5. **Scene** - Fifth paragraph (environment, setting, location) -6. **Movement** - Sixth paragraph (actions, gestures, motion) - -### Implementation Details - -#### Backend (Python) - -**File:** `/nodes/utils/control_panel.py` - -```python -def display_info(self, **kwargs): - # Extract description from all_media_describe_data - description_text = data.get("description", "") - - # Split by double newline to get paragraphs - paragraphs = [p.strip() for p in description_text.split("\n\n") if p.strip()] - - # Map paragraphs to categories (1st = subject, 2nd = cinematic, etc.) - prompt_breakdown = { - "subject": paragraphs[0] if len(paragraphs) > 0 else "", - "cinematic_aesthetic": paragraphs[1] if len(paragraphs) > 1 else "", - "stylization_tone": paragraphs[2] if len(paragraphs) > 2 else "", - "clothing": paragraphs[3] if len(paragraphs) > 3 else "", - "scene": paragraphs[4] if len(paragraphs) > 4 else "", - "movement": paragraphs[5] if len(paragraphs) > 5 else "" - } - - # Return JSON structure - return { - "ui": { - "prompt_breakdown": [json.dumps(prompt_breakdown)], - "raw_data": [all_media_data] - } - } -``` - -**Key Points:** - -- Splits description by `\n\n` (double newline) to identify paragraphs -- Maps paragraphs sequentially to the 6 categories -- Returns `prompt_breakdown` as JSON string in the UI output -- Includes `raw_data` for debugging purposes - -#### Frontend (JavaScript) - -**File:** `/web/js/swiss-army-knife.js` - -```javascript -// Create 6 columns with headings -const subjectCol = createColumn('Subject'); -const cinematicCol = createColumn('Cinematic/Aesthetic'); -const stylizationCol = createColumn('Stylization/Tone'); -const clothingCol = createColumn('Clothing'); -const sceneCol = createColumn('Scene'); -const movementCol = createColumn('Movement'); - -// Parse and display data -this.updatePromptBreakdownData = function (data) { - // Extract prompt_breakdown from data - let rawData = data.prompt_breakdown; - - // Parse JSON if string - let promptBreakdown = - typeof rawData === 'string' ? JSON.parse(rawData) : rawData; - - // Update each column - this._cpb_subject.textContent = promptBreakdown.subject || '(empty)'; - this._cpb_cinematic.textContent = - promptBreakdown.cinematic_aesthetic || '(empty)'; - // ... etc for all 6 columns -}; -``` - -**Key Points:** - -- Creates 6-column flex layout with fixed headers -- Parses `prompt_breakdown` JSON from backend -- Displays each category in its dedicated column -- Shows "(empty)" for missing data -- Includes debug logging for troubleshooting +# Control Panel Prompt Nodes + +## Status Update (November 30, 2025) +- The legacy **ControlPanelOverview** node has been removed. Its functionality overlapped with the ComfyUI viewer widgets and became redundant once the LM Studio nodes started exposing richer `json_output` fields. +- **ControlPanelPromptBreakdown** remains and continues to be the supported control panel node. It focuses on presenting the structured prompt JSON coming from `LLMStudioStructuredDescribe` / `LLMStudioStructuredVideoDescribe`. + +## Active Node: ControlPanelPromptBreakdown +| Attribute | Details | +| --- | --- | +| **Category** | `Swiss Army Knife 🔪/Utils` | +| **Inputs** | `positive_prompt_json` (`STRING`, optional, force input) – expects the `json_output` from LM Studio nodes | +| **Outputs** | UI dictionary only (`{"ui": {"prompt_breakdown": [...]}}`) – no tensor outputs | +| **Purpose** | Split the structured Gemini/LM Studio prompt into five columns (subject, clothing, movement, scene, visual_style) so artists can audit overrides quickly | ### Data Flow - ``` -1. LLMStudioStructuredDescribe Node - ↓ (generates description with 6 paragraphs) - -2. all_media_describe_data (JSON string) - ↓ (contains "description" field) - -3. ControlPanelPromptBreakdown.display_info() - ↓ (splits by \n\n, maps to categories) - -4. UI output: { "prompt_breakdown": [...], "raw_data": [...] } - ↓ - -5. JavaScript widget.onExecuted(message) - ↓ (receives message.prompt_breakdown) - -6. updatePromptBreakdownData(data) - ↓ (parses JSON, updates DOM) - -7. 6-column display in ComfyUI +LM Studio Structured Describe ──▶ ControlPanelPromptBreakdown + json_output positive_prompt_json + │ │ + ├─ (ui) prompt_breakdown JSON ─────┘ → DOM widget renders 5 columns ``` -### Debugging +### Backend Implementation +- `nodes/utils/control_panel.py` + - Class: `ControlPanelPromptBreakdown` + - Function: `display_info` + - Logs with `Logger("ControlPanelPromptBreakdown")` so messages only appear when DEBUG mode is enabled (`DEBUG=true` env or Swiss Army Knife → Settings → Debug toggle). -Both backend and frontend include extensive debug logging: +### Frontend Implementation +- `web/js/swiss-army-knife.js` + - `beforeRegisterNodeDef` hook registers the DOM widget when `nodeData.name === "ControlPanelPromptBreakdown"`. + - Adds five resizable columns with headings plus `Awaiting execution…` placeholders. + - `updatePromptBreakdownData()` tries `prompt_breakdown`, `all_media_describe_data`, and `all_media_describe_data_copy` to remain backward compatible with legacy JSON shapes. + - `onExecuted` pipes `message.output` straight into `updatePromptBreakdownData`. -**Python Backend:** +## Debugging Checklist +1. **Confirm upstream JSON** + - Add a `Note` node or ComfyUI built-in text output to the LM Studio node’s `json_output`. + - Validate that the JSON contains `subject`, `clothing`, `movement`, `scene`, and `visual_style` keys. +2. **Enable DEBUG logs** + ```bash + DEBUG=true python main.py # or set the Swiss Army Knife debug toggle in the UI + ``` + - Look for `[ControlPanelPromptBreakdown DEBUG]` messages to verify parsing and assignment per column. +3. **Browser console** + - Open DevTools → Console. + - Filter for `ControlPanelPromptBreakdown` to ensure `onExecuted` fires and data arrives. +4. **Widget rendering issues** + - The DOM widget width is tied to `nodeType.prototype.onResize`. If the layout looks truncated, drag the node wider; the script recalculates widths each time `onResize` runs. -```python -print(f"\n📊 Prompt Breakdown ({len(paragraphs)} paragraphs found):") -print(f" Subject: {prompt_breakdown['subject'][:100]}...") -# ... logs first 100 chars of each category -``` +## Testing Steps +1. Wire `LLMStudioStructuredDescribe.json_output` to `ControlPanelPromptBreakdown.positive_prompt_json`. +2. Queue the workflow. Expect each column to show the matching paragraph after execution. +3. Delete one field from the JSON and rerun to verify the widget shows `(empty)` for that column. +4. Disable DEBUG mode and confirm the node still renders (logs should be silent). -**JavaScript Frontend:** - -```javascript -console.log('🔍 [ControlPanelPromptBreakdown DEBUG] data received:', data); -console.log( - '🔍 [ControlPanelPromptBreakdown DEBUG] Parsed breakdown:', - promptBreakdown, -); -``` - -### Testing - -1. Connect `LLMStudioStructuredDescribe` `json_output` to `ControlPanelPromptBreakdown` input (wired into `positive_prompt_json`) -2. Execute the workflow -3. Check console logs to verify: - - Description is being split correctly into 6 paragraphs - - JSON structure is valid - - Data reaches the JavaScript widget -4. Verify 6 columns display in the node UI - -### Known Issues - -- If description has fewer than 6 paragraphs, remaining columns show "(empty)" -- No line wrapping controls - long paragraphs may require scrolling -- Column widths are equal (flex: 1) - may need adjustment for different content lengths - -### Future Enhancements - -1. **Smart Paragraph Detection** - - Use AI/heuristics to detect category boundaries if paragraphs don't follow expected order -2. **Column Width Adjustment** - - Allow user to resize columns - - Auto-adjust based on content length -3. **Export Functionality** - - Copy individual categories to clipboard - - Export as structured JSON for prompt engineering tools -4. **Syntax Highlighting** - - Different colors for different categories - - Highlight key terms (e.g., subject names, camera angles, clothing items) - -## Files Modified - -- `/nodes/utils/control_panel.py` - Backend node implementation -- `/web/js/swiss-army-knife.js` - Frontend widget implementation +## Future Enhancements +1. **Column composer** – Allow creators to choose which JSON keys map to columns. +2. **Clipboard helpers** – Quick copy buttons per column for prompt iteration. +3. **Collapsible sections** – Collapse long sections (e.g., scene) to save vertical space. ## Related Documentation - -- [Video Metadata Implementation](../video-metadata/) -- [LM Studio Structured Describe](../lm-studio-describe/) -- [Control Panel Overview](./CONTROL_PANEL_OVERVIEW.md) - -## Version History - -- **2025-10-13**: Initial implementation with 6-column paragraph breakdown +- [dev-docs/web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md](../../web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md) – chronicle of the JSON parsing changes. +- [dev-docs/nodes/lm-studio-describe/](../lm-studio-describe/) – source of the structured prompt schema. +- [web/docs/ControlPanelPromptBreakdown.md](../../../web/docs/ControlPanelPromptBreakdown.md) – user-facing help page that mirrors this information. diff --git a/dev-docs/nodes/control-panel/CONTROL_PANEL_DATA_POPULATION_DEBUG.md b/dev-docs/nodes/control-panel/CONTROL_PANEL_DATA_POPULATION_DEBUG.md new file mode 100644 index 0000000..7267bb0 --- /dev/null +++ b/dev-docs/nodes/control-panel/CONTROL_PANEL_DATA_POPULATION_DEBUG.md @@ -0,0 +1,12 @@ +# Control Panel Data Population Debugging (Archived) + +**Status:** Archived on November 30, 2025. The `ControlPanelOverview` node was removed, so this guide is kept only for legacy workflows that still reference the retired node. Modern workflows should use `ControlPanelPromptBreakdown` and the steps documented in `CONTROL_PANEL.md`. + +Legacy troubleshooting checklist (for reference only): + +1. Enable `DEBUG=true` so the Python logger prints incoming kwargs from `ControlPanelOverview`. +2. Check browser DevTools for `[ControlPanel DEBUG]` logs verifying that `message.output.all_media_describe_data` exists. +3. Ensure upstream nodes emit `all_media_describe_data` JSON strings; otherwise the UI displayed "No inputs connected". +4. Validate FFmpeg/metadata helpers if the right column stayed empty—most issues stemmed from missing `media_info`, `height`, or `width` keys. + +> If you still have a workflow that uses `ControlPanelOverview`, delete the node or swap it for `ControlPanelPromptBreakdown`. Keeping the old node in the graph will now show a missing-node placeholder inside ComfyUI. diff --git a/dev-docs/nodes/control-panel/README.md b/dev-docs/nodes/control-panel/README.md index 5ba1f3d..135ae3b 100644 --- a/dev-docs/nodes/control-panel/README.md +++ b/dev-docs/nodes/control-panel/README.md @@ -1,112 +1,62 @@ # Control Panel Node Documentation -The Control Panel node is a display-only node that shows comprehensive workflow information from connected nodes. -## 📄 Documentation +## Files +- **[CONTROL_PANEL.md](CONTROL_PANEL.md)** – canonical implementation details for `ControlPanelPromptBreakdown` (backend + frontend) +- **CONTROL_PANEL_DATA_POPULATION_DEBUG.md** – archived notes from the retired `ControlPanelOverview` node (kept only for historical troubleshooting of legacy workflows) -- **[CONTROL_PANEL.md](CONTROL_PANEL.md)** - Complete consolidated documentation - -## Documentation Files - -### CONTROL_PANEL_DATA_POPULATION_DEBUG.md - -**Enhanced debugging for data population issues** - -Comprehensive guide to debugging Control Panel data flow from backend to frontend. Includes: - -- Double output implementation (redundant data paths) -- Extensive console logging for both Python and JavaScript -- Step-by-step debugging workflow -- Common issues and solutions -- Testing procedures - -**Use when**: Control Panel is not showing data or you need to trace data flow through the system. - -**Date**: October 11, 2025 -**Status**: ✅ Implemented - ---- - -## Node Overview +## Active Node Summary +| Property | Value | +| --- | --- | +| **Name** | `ControlPanelPromptBreakdown` | +| **Category** | Swiss Army Knife 🔪/Utils | +| **Inputs** | `positive_prompt_json` (`STRING`, optional, force input) | +| **Outputs** | UI-only (`{"ui": {"prompt_breakdown": [...]}}`) | +| **Status** | Stable | ### Purpose +Render the structured prompt JSON from LM Studio nodes into five digestible columns (Subject, Clothing, Movement, Scene, Visual Style). It is the fastest way to sanity-check overrides before feeding prompts downstream. -The Control Panel node acts as a central dashboard that displays key workflow information in an organized, multi-column layout. +### Input Schema Expectations +- Source JSON: `LLMStudioStructuredDescribe.json_output` or `LLMStudioStructuredVideoDescribe.json_output` +- Required keys: `subject`, `clothing`, `movement`, `scene`, `visual_style` +- Backward compatibility: still inspects `all_media_describe_data`/`prompt_breakdown` arrays so older saved workflows can load without edits. -### Features +### Output Behavior +- The node does **not** emit tensors. Instead it returns a `ui` dictionary that ComfyUI reads to populate the DOM widget created in `web/js/swiss-army-knife.js`. +- Missing keys render as `(empty)` so artists know which paragraphs need overrides. -- **Display-only node** - No outputs, only visualizes data -- **Three-column layout**: - - Left: Final Prompt/Description (full text) - - Middle: Gemini Status (model, API key info, completion status) - - Right: Media Info (media processing details, height, width only) -- **Accepts JSON data** via `all_media_describe_data` input -- **Auto-updates** on workflow execution -- **Clean display** - Shows only essential metadata, hides verbose fields like subject, cinematic_aesthetic, stylization_tone, clothing, scene, movement - -### Input - -- `all_media_describe_data` (STRING, optional) - JSON string containing workflow data - -### Output - -None - display-only node - -### Category - -Swiss Army Knife 🔪/Utils - -## Implementation Files - -### Backend - -- `nodes/utils/control_panel.py` - Python node implementation - - Class: `ControlPanel` - - Function: `display_info` - - Returns: `{"ui": {"all_media_describe_data": [...]}}` - -### Frontend - -- `web/js/swiss-army-knife.js` - JavaScript widget implementation - - Lines ~238-520: Control Panel node setup - - Function: `updateControlPanelData` - Parses and displays data - - Handler: `onExecuted` - Receives execution results - -## Data Flow +## Implementation Locations +- **Backend**: `nodes/utils/control_panel.py` → `ControlPanelPromptBreakdown.display_info` +- **Frontend**: `web/js/swiss-army-knife.js` → `beforeRegisterNodeDef` handler for `ControlPanelPromptBreakdown` +## Data Flow Overview ``` -Gemini Media Describe Node - ↓ - all_media_describe_data (JSON string) - ↓ -Control Panel Node (Python) - ↓ - {"ui": {"all_media_describe_data": [...]}} - ↓ -ComfyUI onExecuted Event - ↓ - message.output.all_media_describe_data - ↓ -JavaScript updateControlPanelData() - ↓ - Parse JSON → Extract fields → Update DOM - ↓ -Three-column display in UI +LLMStudioStructuredDescribe ──▶ ControlPanelPromptBreakdown + json_output positive_prompt_json + │ │ + └─────────────── JSON string ───┘ + │ + Python parses JSON + │ + {"ui": {"prompt_breakdown": [...]}} + │ + JS updatePromptBreakdownData() + │ + DOM columns render text ``` ## Related Nodes +- **LM Studio Structured Describe (Image/Video)** – produces the JSON consumed here +- **Control Panel Prompt Breakdown (web/docs)** – user-facing help that mirrors the developer view +- **Media Selection** – typically precedes LM Studio nodes and controls which asset is being described -- **Gemini Media Describe** - Primary data source for Control Panel -- **Media Selection** - Provides media path information -- **Video Metadata** - Provides technical video details +## Troubleshooting Quick Hits +1. **No columns populated** → Confirm the JSON actually reaches the node (`DEBUG=true` and check `[ControlPanelPromptBreakdown DEBUG]` logs). +2. **Columns show `(No data available)`** → The upstream JSON is empty or missing required keys. Verify the LM Studio node executed successfully. +3. **Widget squashed** → Drag the node wider; the DOM widget recomputes widths on every resize. +4. **Legacy workflows** → If an ancient workflow still references `ControlPanelOverview`, replace it with `ControlPanelPromptBreakdown` or remove the node entirely. -## Known Issues - -See `CONTROL_PANEL_DATA_POPULATION_DEBUG.md` for debugging data population issues. - -## Future Enhancements - -- [ ] Add configurable column layout -- [ ] Support custom field selection -- [ ] Add export functionality (copy to clipboard, save to file) -- [ ] Add collapsible sections for large datasets -- [ ] Add separate `CONTROL_PANEL_DEBUG` toggle independent of global DEBUG \ No newline at end of file +## Future Ideas +- Column selector (let artists pick which JSON keys to visualize) +- Copy-to-clipboard buttons per column +- Optional markdown rendering for the visual style column diff --git a/dev-docs/nodes/lora-info-extractor/LORA_INFO_EXTRACTOR.md b/dev-docs/nodes/lora-info-extractor/LORA_INFO_EXTRACTOR.md index 9d80a19..59549b0 100644 --- a/dev-docs/nodes/lora-info-extractor/LORA_INFO_EXTRACTOR.md +++ b/dev-docs/nodes/lora-info-extractor/LORA_INFO_EXTRACTOR.md @@ -1,6 +1,6 @@ # LoRA Info Extractor Node -The **LoRA Info Extractor** walks WanVideo LoRA stacks, calculates persistent hashes for every referenced file, optionally fetches matching metadata from CivitAI, and emits both a structured JSON blob plus a readable summary string. Use it to keep render logs auditable and to pass rich LoRA metadata into downstream nodes such as the Video Metadata updater. +The **LoRA Info Extractor** walks WanVideo LoRA stacks, calculates persistent hashes for every referenced file, optionally fetches matching metadata from CivitAI, and emits both a structured JSON blob plus a readable summary string. Use it to keep render logs auditable and to pass rich LoRA metadata into downstream logging and archival nodes. ## Features @@ -80,15 +80,15 @@ All inputs are optional except `lora`. The node automatically handles single dic } ``` -Use the `combined_display` string as a quick label (e.g., send it into the Video Metadata node so FFmpeg titles/keywords list the LoRAs that were used). +Use the `combined_display` string as a quick label (e.g., hand it to your FFmpeg/metadata script so rendered files list the LoRAs that were used). ## Typical Workflow ``` -WanVideo LoRA Select (Multi) ──▶ LoRAInfoExtractor ──▶ VideoMetadataNode - │ │ - ├─ lora_json ────────┘ (metadata embed) - ├─ lora_info ──▶ Control Panel / Logger +WanVideo LoRA Select (Multi) ──▶ LoRAInfoExtractor + │ + ├─ lora_json ──▶ FFmpeg/Azure upload scripts for metadata embedding + ├─ lora_info ──▶ Control Panel / built-in text display nodes └─ lora_passthrough ─▶ Downstream LoRA-aware nodes ``` @@ -103,13 +103,13 @@ WanVideo LoRA Select (Multi) ──▶ LoRAInfoExtractor ──▶ VideoMetadata | Issue | Fix | | --- | --- | -| `Fallback: No LoRAs Detected` | Ensure the upstream node outputs `WANVIDLORA` (list/dict). You can wire `lora_passthrough` into a `ShowText` node to inspect the raw stack. | +| `Fallback: No LoRAs Detected` | Ensure the upstream node outputs `WANVIDLORA` (list/dict). You can wire `lora_passthrough` into ComfyUI’s built-in text display nodes to inspect the raw stack. | | Missing files | The summary line reports `Missing: N`. Confirm the referenced `.safetensors` files exist at the recorded path. | | No CivitAI matches | Confirm the key is set in ComfyUI settings and `use_civitai_api=True`. The debug log also prints whether a key was detected. | | Slow first run | Hash cache warms on the first pass because every LoRA file must be read. Subsequent runs re-use cached hashes as long as the files remain unchanged. | ## Related Documentation -- [Video Metadata Node](../video-metadata/VIDEO_METADATA.md) – Shows how `lora_json` is embedded back into FFmpeg metadata. - [CivitAI API Key Widget Implementation](../../integrations/civitai/CIVITAI_API_KEY_WIDGET.md) – Details the backend/frontend plumbing for settings-based API keys. - [Settings Integration Guide](../../infrastructure/SETTINGS_INTEGRATION.md) – Overview of how Swiss Army Knife nodes access stored secrets. +- [Azure Storage Upload](../azure-storage-upload/AZURE_STORAGE_UPLOAD.md) – Example of pushing structured metadata and assets to the cloud. diff --git a/dev-docs/nodes/lora-info-extractor/README.md b/dev-docs/nodes/lora-info-extractor/README.md index 3612b63..2d5887b 100644 --- a/dev-docs/nodes/lora-info-extractor/README.md +++ b/dev-docs/nodes/lora-info-extractor/README.md @@ -6,7 +6,7 @@ Extracts metadata about Wan LoRA stacks, hashes every file, and optionally enric 1. Connect `WANVIDLORA` output from WanVideo LoRA Select (single or multi). 2. (Optional) Toggle `use_civitai_api` if you want to hit the CivitAI API using the Swiss Army Knife settings key. -3. Wire the `lora_json` output into downstream nodes (e.g., Video Metadata) and surface `lora_info` anywhere you want a readable summary. +3. Wire the `lora_json` output into downstream metadata/logging nodes and surface `lora_info` anywhere you want a readable summary. ## Inputs diff --git a/dev-docs/nodes/reddit-media/README.md b/dev-docs/nodes/reddit-media/README.md index 1fd4bc4..08bf5c8 100644 --- a/dev-docs/nodes/reddit-media/README.md +++ b/dev-docs/nodes/reddit-media/README.md @@ -72,7 +72,7 @@ Extract media from: ## 📚 Related Documentation -- [Video Metadata](../video-metadata/) - For extracting video information +- [Media Selection](../media-selection/) - Route downloaded assets into structured describe nodes - [Features](../../features/) - For text extraction and processing --- diff --git a/dev-docs/nodes/video-metadata/README.md b/dev-docs/nodes/video-metadata/README.md deleted file mode 100644 index 4390cb4..0000000 --- a/dev-docs/nodes/video-metadata/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Video Metadata Node Documentation - -The Video Metadata node extracts, manipulates, and appends metadata to videos during processing. - -## 📄 Documentation - -- **[VIDEO_METADATA.md](VIDEO_METADATA.md)** - Complete documentation including append functionality, VHS compatibility, LoRA JSON integration, and troubleshooting - -## 🎯 Quick Reference - -### Node Purpose - -Extract and manipulate video metadata including: - -- Resolution and dimensions -- Frame rate and duration -- Codec information -- Custom metadata fields -- Workflow integration data - -### Key Features - -- **Metadata Extraction**: Read metadata from video files -- **JSON Integration**: Export/import metadata as JSON -- **Append Functionality**: Add metadata to existing videos -- **VHS Compatibility**: Works seamlessly with VHS Video nodes - -## 🔧 Technical Details - -### Files - -- **Python Backend**: `nodes/utils/video_metadata.py` -- **Metadata Format**: JSON-based metadata structure - -### Metadata Structure - -```json -{ - "width": 1080, - "height": 1920, - "fps": 30, - "duration": 10.5, - "codec": "h264", - "custom_fields": {} -} -``` - -## 📚 Related Documentation - -- [LM Studio Structured Describe Nodes](../lm-studio-describe/) - For AI-generated descriptions - ---- - -**Node Type**: Utility/Metadata -**Category**: Video Processing -**Status**: Stable diff --git a/dev-docs/nodes/video-metadata/VIDEO_METADATA.md b/dev-docs/nodes/video-metadata/VIDEO_METADATA.md deleted file mode 100644 index 4a77d10..0000000 --- a/dev-docs/nodes/video-metadata/VIDEO_METADATA.md +++ /dev/null @@ -1,404 +0,0 @@ -# Update Video Metadata Node - -**Last Updated:** October 13, 2025 -**Node Type:** Metadata Processor -**Category:** Utils -**Display Name:** Update Video Metadata -**Status:** Active Development - -## Overview - -The Update Video Metadata node enables adding and appending metadata to video files using FFmpeg. It's designed to work seamlessly with the VHS_VideoCombine node and other video processing nodes in ComfyUI workflows. The node **appends to existing metadata** instead of overwriting it, ensuring that existing metadata in video files is preserved while adding new information. - -## Table of Contents - -- [Features](#features) -- [Input Parameters](#input-parameters) -- [Usage Examples](#usage-examples) -- [Metadata Appending Behavior](#metadata-appending-behavior) -- [LoRA JSON Integration](#lora-json-integration) -- [VHS VideoCombine Compatibility](#vhs-videocombine-compatibility) -- [Technical Implementation](#technical-implementation) -- [Error Handling](#error-handling) -- [Troubleshooting](#troubleshooting) -- [Future Enhancements](#future-enhancements) - -## Features - -- **FFmpeg-based metadata injection**: Uses FFmpeg's `-metadata` option for reliable metadata embedding -- **Append mode**: Preserves existing metadata while adding new information -- **Chain-friendly design**: Takes filename input and outputs filename for workflow chaining -- **Non-destructive option**: Can create new files with metadata instead of overwriting originals -- **Copy-only processing**: Uses stream copying to avoid re-encoding (fast and lossless) -- **Multiple metadata fields**: Supports artist, comment, title, description, keywords -- **LoRA JSON integration**: Automatically extracts metadata from LoRA JSON data -- **VHS_FILENAMES type support**: Direct compatibility with VHS VideoCombine node - -## Input Parameters - -### Required - -- **filenames**: (VHS_FILENAMES, forceInput) - Video filenames from VHS_VideoCombine or other video output nodes - - Supports single string filename - - Supports list with single file (extracts first) - - Supports list with multiple files (processes first file only) - -### Optional - -- **artist**: (STRING) - Artist/Creator metadata -- **comment**: (STRING, multiline) - Additional comments metadata -- **overwrite_original**: (Yes/No) - Whether to overwrite the original file or create a new one with '_metadata' suffix - -## Output - -- **filenames**: (VHS_FILENAMES) - Output filenames (original or new file with metadata) - -## Usage Examples - -### Basic Usage with VHS_VideoCombine - -``` -[Video Processing] → [VHS_VideoCombine] → [Update Video Metadata] → [Output] - ↑ - [artist, comment] -``` - -1. Connect the `Filenames` output from VHS_VideoCombine to the `filenames` input -2. Optionally add artist and comment metadata -3. Choose whether to overwrite original or create new file -4. The output `filenames` can be connected to other nodes or used as final output - -### Multi-Stage Processing - -``` -[VHS_VideoCombine] → [Update Video Metadata] → [Update Video Metadata] → [Output] - ↑ ↑ - [Artist info] [LoRA info] -``` - -Multiple Update Video Metadata nodes can be chained to add metadata in stages. Each stage appends to existing metadata. - -## Metadata Appending Behavior - -The node **appends** new metadata to existing metadata instead of overwriting it. - -### Example: Adding LoRA Info to Video with Existing Metadata - -**Input Video Metadata:** -``` -title: "Character Animation" -description: "Fantasy character design" -artist: "Original Creator" -``` - -**Node Parameters:** -``` -lora_json: {"combined_display": "Style + Detail LoRAs", ...} -``` - -**Output Video Metadata:** -``` -title: "Character Animation - -Style + Detail LoRAs" - -description: "Fantasy character design - -LoRA Information: -• Local: Style LoRA (strength: 0.8) -• Local: Detail LoRA (strength: 0.6)" - -artist: "Original Creator" -keywords: "LoRA: Style, Detail" -lora_json: "{...full LoRA JSON...}" -``` - -### Metadata Combination Rules - -- **Title, Description, Comment**: Separated with double newlines (`\n\n`) for readability -- **Artist**: Appended with double newlines -- **Keywords**: Comma-separated for better keyword handling - -## VHS VideoCombine Compatibility - -The node uses `VHS_FILENAMES` type for full compatibility with the VHS (VideoHelperSuite) ecosystem. - -### Type Compatibility - -```python -# VHS VideoCombine Output -class VideoCombineNode: - RETURN_TYPES = ("VHS_FILENAMES",) - RETURN_NAMES = ("Filenames",) - -# Update Video Metadata Input/Output -class VideoMetadataNode: - INPUT_TYPES = { - "required": { - "filenames": ("VHS_FILENAMES", {"forceInput": True}) - } - } - RETURN_TYPES = ("VHS_FILENAMES",) -``` - -### Input Format Support - -| Input Format | Support | Behavior | -| ------------------------ | ------- | -------------------------- | -| Single string filename | ✅ | Direct processing | -| List with single file | ✅ | Extract first file | -| List with multiple files | ✅ | Process first file only | -| Empty/invalid input | ❌ | Error with helpful message | - -### Processing Logic - -```python -# Flexible input handling -if isinstance(filenames, list) and len(filenames) > 0: - filename = filenames[0] # Use first file if multiple -elif isinstance(filenames, str): - filename = filenames # Direct string filename -else: - raise Exception(f"Invalid filenames input: {filenames}") -``` - -## Technical Implementation - -### FFmpeg Command Structure - -The node constructs FFmpeg commands like: - -```bash -# Basic metadata with appending -ffmpeg -i input.mp4 -c copy \ - -metadata artist="Original Creator - -AI Creator Studio" \ - -metadata comment="Original notes - -Additional production info" \ - output.mp4 - -# With LoRA JSON data (automatically generated and appended) -ffmpeg -i input.mp4 -c copy \ - -metadata title="Original Title - -LoRA1 + LoRA2" \ - -metadata description="Original description - -LoRA Information: -• CivitAI: LoRA1 by Creator1 -• Local: LoRA2 (strength: 0.8)" \ - -metadata keywords="original, keywords, LoRA: LoRA1, LoRA2" \ - -metadata lora_json='{"loras":[...],"count":2,"combined_display":"LoRA1 + LoRA2"}' \ - output.mp4 -``` - -### Helper Methods - -#### `_get_existing_metadata(filename)` - -Reads existing metadata from video file using ffprobe: - -```python -def _get_existing_metadata(self, filename): - """Read existing metadata from video file using ffprobe.""" - cmd = [ - 'ffprobe', '-v', 'quiet', '-print_format', 'json', - '-show_format', filename - ] - # Extracts existing metadata tags - # Returns dictionary of existing metadata fields - # Gracefully handles missing files or invalid metadata -``` - -#### `_combine_metadata_field(existing, new)` - -Combines existing and new metadata content: - -```python -def _combine_metadata_field(self, existing, new): - """Combine existing metadata field with new content.""" - if not existing: - return new - if not new: - return existing - # Separates content with double newlines for readability - return f"{existing}\n\n{new}" -``` - -### Key Design Decisions - -1. **Stream Copying (-c copy)**: Avoids re-encoding for speed and quality preservation -2. **Conditional Metadata**: Only adds metadata fields that have content -3. **Safe Overwriting**: Uses temporary files when overwriting to prevent corruption -4. **Error Handling**: Comprehensive error handling for file operations and FFmpeg execution -5. **Metadata Preservation**: Reads existing metadata before writing to preserve it - -### File Handling - -- **Overwrite Mode**: Creates temporary file, then replaces original -- **New File Mode**: Creates new file with `_metadata` suffix -- **Validation**: Checks input file existence before processing -- **Cleanup**: Removes temporary files on error - -## Error Handling - -The node handles several error conditions: - -### Missing Input File - -```python -if not os.path.exists(filename): - raise Exception(f"Input video file not found: {filename}") -``` - -### Metadata Reading Failures - -```python -# Returns empty dict if metadata can't be read -except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError): - return {} # Fresh metadata will be created -``` - -### FFmpeg Execution Errors - -```python -# Clear error messages for FFmpeg issues -except subprocess.CalledProcessError as e: - raise Exception(f"FFmpeg metadata operation failed: {e.stderr}") -``` - -### Invalid Input Type - -```python -# Handles various input formats gracefully -if isinstance(filenames, list) and len(filenames) > 0: - filename = filenames[0] -elif isinstance(filenames, str): - filename = filenames -else: - raise Exception(f"Invalid filenames input: {filenames}") -``` - -## Troubleshooting - -### Common Issues - -1. **FFmpeg not found**: - - Ensure FFmpeg is installed and in PATH - - Verify with: `ffmpeg -version` - -2. **Permission denied**: - - Check file permissions for input/output files - - Ensure write permissions in output directory - -3. **Unsupported format**: - - Verify video format is supported by FFmpeg - - Test with common formats: MP4, MOV, MKV, WebM - -4. **Disk space**: - - Ensure sufficient space for output files - - Especially important when not overwriting originals - -5. **Type mismatch error**: - - Ensure using VHS_FILENAMES type from VHS VideoCombine - - Check that input is properly connected - -### Debug Information - -The node outputs success messages and detailed error information: -- FFmpeg command being executed -- Input/output file paths -- Metadata being added -- Error messages with stderr output - -## Benefits - -### Data Preservation - -- ✅ Existing metadata is never lost -- ✅ Multiple processing passes accumulate information -- ✅ Original authorship and descriptions preserved - -### Enhanced Workflow Support - -- ✅ Can process videos through multiple metadata nodes -- ✅ Different stages can add different types of metadata -- ✅ Supports iterative workflow development - -### Better Organization - -- ✅ Clear separation between different metadata sources -- ✅ Readable formatting with double newlines -- ✅ Keywords properly comma-separated - -### Backward Compatibility - -- ✅ Works with videos that have no existing metadata -- ✅ Graceful handling of missing or corrupt metadata -- ✅ Compatible with VHS VideoCombine and custom nodes - -## Compatibility - -### Video Formats - -Supports all video formats that FFmpeg can handle: -- MP4, MOV, AVI, MKV, WebM, FLV, etc. - -### Metadata Standards - -- Uses standard metadata keys compatible with most video players -- Follows FFmpeg metadata conventions -- Compatible with QuickTime, MP4, and MKV metadata standards - -### ComfyUI Integration - -- Compatible with ComfyUI's type system (VHS_FILENAMES) -- Works with VHS node suite and other video processing nodes -- Supports workflow chaining and multi-stage processing - -## Performance Considerations - -- **No Re-encoding**: Uses stream copying for maximum speed -- **Minimal Memory Usage**: FFmpeg handles file processing efficiently -- **Batch Processing**: Can be used in loops for multiple files -- **Fast Metadata Operations**: Metadata changes are nearly instant - -## Future Enhancements - -### Potential Improvements - -1. **Metadata Versioning**: Track when different metadata was added -2. **Selective Overwrite**: Option to overwrite specific fields while preserving others -3. **Metadata Templates**: Predefined metadata structures for different use cases -4. **Batch Processing**: Process multiple files with same metadata updates -5. **Custom metadata key support**: Add arbitrary metadata fields -6. **Metadata extraction/reading capabilities**: Read and display existing metadata -7. **Preview of existing metadata**: Show what's already in the video file - -### Advanced Features - -1. **Metadata Validation**: Ensure metadata formats are valid -2. **Custom Separators**: Different separation styles for different metadata types -3. **Metadata Export**: Export metadata to external formats (JSON, XML, etc.) -4. **History Tracking**: Maintain history of metadata changes -5. **Integration with other metadata standards**: Support for XMP, EXIF, etc. - -## Related Documentation - -- [Structured Describe Nodes](../lm-studio-describe/) - For AI-powered video analysis -- [VHS VideoCombine](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) - Video combination node - -## Installation Notes - -- Requires FFmpeg to be installed and accessible in system PATH -- No additional Python dependencies beyond what's already in the project -- Compatible with existing ComfyUI-SwissArmyKnife installation - ---- - -**Node Type**: Metadata Processor -**Category**: Utils -**Status**: Active Development -**VHS Compatible**: ✅ Yes diff --git a/dev-docs/web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md b/dev-docs/web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md index 745bc3b..e77d610 100644 --- a/dev-docs/web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md +++ b/dev-docs/web-js/CONTROL_PANEL_PROMPT_BREAKDOWN_JSON_UPDATE.md @@ -100,7 +100,6 @@ Or when not connected: ``` LLMStudioStructuredDescribe / LLMStudioStructuredVideoDescribe - ├─ json_output → Control Panel Overview (optional display) └─ json_output → Control Panel Prompt Breakdown (wire into `positive_prompt_json`) ``` diff --git a/nodes/helper_nodes.py b/nodes/helper_nodes.py index 37883dc..c0068ea 100644 --- a/nodes/helper_nodes.py +++ b/nodes/helper_nodes.py @@ -1,56 +1,2 @@ -import torch -from .debug_utils import Logger - -logger = Logger("HelperNodes") -class ShowText: - """ - A simple ComfyUI custom node that displays text input. - Useful for showing the final_string output from Gemini nodes. - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - """ - Return a dictionary which contains config for all input fields. - Takes a STRING input and displays it. - """ - return { - "required": { - "text": ("STRING", { - "multiline": True, - "default": "", - "tooltip": "Text to display" - }), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) - FUNCTION = "show_text" - CATEGORY = "Swiss Army Knife 🔪/Utils" - OUTPUT_NODE = True # This makes it an output node that shows in the UI - DESCRIPTION = ( - "Simple output helper that prints and displays any STRING input inside ComfyUI—ideal for inspecting prompts or captions " - "produced by the structured describe nodes." - ) - - def show_text(self, text): - """ - Simply return the input text and display it - """ - logger.log(f"ShowText node displaying: {text[:100]}...") # Log first 100 chars - return (text,) - - -# A dictionary that contains the helper nodes -HELPER_NODE_CLASS_MAPPINGS = { - "ShowText": ShowText -} - -# A dictionary that contains the friendly/humanly readable titles for the nodes -HELPER_NODE_DISPLAY_NAME_MAPPINGS = { - "ShowText": "Show Text" -} +HELPER_NODE_CLASS_MAPPINGS = {} +HELPER_NODE_DISPLAY_NAME_MAPPINGS = {} diff --git a/nodes/nodes.py b/nodes/nodes.py index d21cdb6..3e48ea0 100644 --- a/nodes/nodes.py +++ b/nodes/nodes.py @@ -1,354 +1,17 @@ -import os -import subprocess -import json - -from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple - -from .civitai_service import CivitAIService -from .lora_hash_cache import get_cache as get_lora_hash_cache from .media_describe import ( LLMStudioStructuredDescribe, LLMStudioStructuredVideoDescribe, ) -from .debug_utils import Logger - -logger = Logger("Nodes") - - -class FilenameGenerator: - """ - A ComfyUI custom node that generates structured filenames based on workflow parameters. - Creates filenames with optional date subdirectory and all parameter values included. - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - """ - Return a dictionary which contains config for all input fields. - """ - return { - "required": { - "scheduler": ("STRING", { - "forceInput": True, - "tooltip": "Scheduler input from WanVideo Scheduler Selector or other scheduler nodes" - }), - "shift": ("FLOAT", { - "default": 12.0, - "min": 0.0, - "max": 100.0, - "step": 0.01, - "tooltip": "Shift value" - }), - "total_steps": ("INT", { - "default": 40, - "min": 1, - "max": 10000, - "tooltip": "Total number of steps" - }), - "shift_step": ("INT", { - "default": 25, - "min": 1, - "max": 10000, - "tooltip": "Shift step value" - }), - "high_cfg": ("FLOAT", { - "default": 3.0, - "min": 0.0, - "max": 30.0, - "step": 0.01, - "tooltip": "High CFG value" - }), - "low_cfg": ("FLOAT", { - "default": 4.0, - "min": 0.0, - "max": 30.0, - "step": 0.01, - "tooltip": "Low CFG value" - }), - "base_filename": ("STRING", { - "default": "base", - "tooltip": "Base filename (without extension)" - }), - "subdirectory_prefix": ("STRING", { - "default": "", - "tooltip": "Optional subdirectory prefix (e.g., 'project_name'). Will be added before date subdirectory." - }), - "add_date_subdirectory": ("BOOLEAN", { - "default": True, - "tooltip": "Add date subdirectory (YYYY-MM-DD format)" - }), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("filename",) - FUNCTION = "generate_filename" - CATEGORY = "Swiss Army Knife 🔪/Utils" - DESCRIPTION = ( - "Builds a structured filename/path from scheduler, CFG, and Wan parameters (plus optional prefixes/date folders) so " - "saved outputs remain organized." - ) - - def generate_filename(self, scheduler, shift, total_steps, shift_step, high_cfg, low_cfg, base_filename, subdirectory_prefix, add_date_subdirectory): - """ - Generate a structured filename based on the provided parameters. - Args: - scheduler: Scheduler string from scheduler node - shift: Shift value - total_steps: Total number of steps - shift_step: Shift step value - high_cfg: High CFG value - low_cfg: Low CFG value - base_filename: Base filename - subdirectory_prefix: Optional subdirectory prefix - add_date_subdirectory: Whether to add date subdirectory - Returns: - Generated filename string - """ - try: - # Format float values to replace decimal points with underscores - shift_str = f"{shift:.2f}".replace(".", "_") - high_cfg_str = f"{high_cfg:.2f}".replace(".", "_") - low_cfg_str = f"{low_cfg:.2f}".replace(".", "_") - - # Clean scheduler string to ensure it's filename-safe - scheduler_clean = str(scheduler).strip().replace(" ", "_").lower() - - # Clean base filename to ensure it's filename-safe - base_clean = base_filename.strip().replace(" ", "_") - - # Generate the filename components - filename_parts = [ - base_clean, - "scheduler", scheduler_clean, - "shift", shift_str, - "total_steps", str(total_steps), - "shift_step", str(shift_step), - "highCFG", high_cfg_str, - "lowCFG", low_cfg_str - ] - - # Join all parts with underscores - filename = "_".join(filename_parts) - - # Build directory structure with optional subdirectory prefix and date - directory_parts = [] - - # Add subdirectory prefix if provided - if subdirectory_prefix and subdirectory_prefix.strip(): - prefix_clean = subdirectory_prefix.strip().replace(" ", "_") - directory_parts.append(prefix_clean) - - # Add date subdirectory if requested - if add_date_subdirectory: - current_date = datetime.now().strftime("%Y-%m-%d") - directory_parts.append(current_date) - - # Combine directory parts with filename - if directory_parts: - full_path = "/".join(directory_parts) + "/" + filename - else: - full_path = filename - - return (full_path,) - - except Exception as e: - raise Exception(f"Filename generation failed: {str(e)}") - - -class VideoMetadataNode: - """ - A ComfyUI custom node for adding metadata to video files. - Takes a filename input (typically from VHS_VideoCombine) and adds custom metadata - using FFmpeg. Supports common metadata fields like title, description, artist, and keywords. - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - """ - Return a dictionary which contains config for all input fields. - """ - return { - "required": { - "filenames": ("VHS_FILENAMES", { - "forceInput": True, - "tooltip": "Video filenames from VHS_VideoCombine (VHS_FILENAMES type)" - }), - }, - "optional": { - "comment": ("STRING", { - "multiline": True, - "default": "", - "tooltip": "Additional comments metadata" - }), - "overwrite_original": (["Yes", "No"], { - "default": "No", - "tooltip": "Whether to overwrite the original file or create a new one with '_metadata' suffix" - }), - } - } - - RETURN_TYPES = ("VHS_FILENAMES",) - RETURN_NAMES = ("Filenames",) - FUNCTION = "add_metadata" - CATEGORY = "Swiss Army Knife 🔪/Utils" - DESCRIPTION = ( - "Adds artist/comments/LoRA metadata to videos coming from VHS_VideoCombine via ffmpeg copy, outputting the updated " - "filename list for downstream saves." - ) - - def add_metadata(self, filenames, artist="", comment="", overwrite_original="No"): - """ - Update metadata in a video file using FFmpeg, appending to existing metadata. - - Args: - filenames: Input video filenames from VHS_VideoCombine (will process first file) - artist: Artist/Creator name (appends to existing artist info) - comment: Additional comments (appends to existing comments) - overwrite_original: Whether to overwrite original file - - Returns: - Output filenames (original or new file with updated metadata) - """ - try: - # Extract filename from filenames input (VHS_VideoCombine may output multiple files or special format) - # Handle both single filename string and potential list/array formats - if isinstance(filenames, list) and len(filenames) > 0: - filename = filenames[0] # Use first file if multiple - elif isinstance(filenames, str): - filename = filenames # Direct string filename - else: - raise Exception(f"Invalid filenames input: {filenames}") - - # Validate input file exists - if not os.path.exists(filename): - raise Exception(f"Input video file not found: {filename}") - - # Read existing metadata first - existing_metadata = self._get_existing_metadata(filename) - - # Determine output filename - if overwrite_original == "Yes": - output_filename = filename - temp_filename = filename + ".tmp" - else: - # Create new filename with _metadata suffix - name, ext = os.path.splitext(filename) - output_filename = f"{name}_metadata{ext}" - temp_filename = output_filename - - # Build FFmpeg command with metadata - cmd = [ - 'ffmpeg', - '-i', filename, - '-c', 'copy', # Copy streams without re-encoding - '-y' # Overwrite output file if it exists - ] - - # Add basic metadata options if provided (append to existing) - if artist.strip(): - existing_artist = existing_metadata.get('artist', '') - combined_artist = self._combine_metadata_field(existing_artist, artist.strip()) - cmd.extend(['-metadata', f'artist={combined_artist}']) - - if comment.strip(): - existing_comment = existing_metadata.get('comment', '') - combined_comment = self._combine_metadata_field(existing_comment, comment.strip()) - cmd.extend(['-metadata', f'comment={combined_comment}']) - - # Add output filename - cmd.append(temp_filename) - - # Execute FFmpeg command - subprocess.run(cmd, capture_output=True, text=True, check=True) - - # If we're overwriting the original, move temp file to original location - if overwrite_original == "Yes": - os.replace(temp_filename, output_filename) - - logger.log(f"Successfully updated metadata in video: {output_filename}") - return (output_filename,) - - except subprocess.CalledProcessError as e: - raise Exception(f"FFmpeg metadata operation failed: {e.stderr}") - except Exception as e: - raise Exception(f"Video metadata operation failed: {str(e)}") - - def _get_existing_metadata(self, filename): - """ - Read existing metadata from video file using ffprobe. - - Args: - filename: Path to video file - - Returns: - Dictionary of existing metadata fields - """ - try: - cmd = [ - 'ffprobe', - '-v', 'quiet', - '-print_format', 'json', - '-show_format', - filename - ] - - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - metadata_info = json.loads(result.stdout) - - # Extract metadata tags from format section - if 'format' in metadata_info and 'tags' in metadata_info['format']: - return metadata_info['format']['tags'] - else: - return {} - - except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError): - # If we can't read metadata, return empty dict (metadata will be created fresh) - return {} - - def _combine_metadata_field(self, existing, new): - """ - Combine existing metadata field with new content. - - Args: - existing: Existing metadata content (string) - new: New metadata content to append (string) - - Returns: - Combined metadata string - """ - existing = existing.strip() if existing else '' - new = new.strip() if new else '' - - if not existing: - return new - elif not new: - return existing - else: - # Separate with double newline for readability - return f'{existing}\n\n{new}' - # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { "LLMStudioStructuredDescribe": LLMStudioStructuredDescribe, "LLMStudioStructuredVideoDescribe": LLMStudioStructuredVideoDescribe, - "FilenameGenerator": FilenameGenerator, - "VideoMetadataNode": VideoMetadataNode } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { "LLMStudioStructuredDescribe": "LM Studio Structured Describe (Image)", "LLMStudioStructuredVideoDescribe": "LM Studio Structured Describe (Video)", - "FilenameGenerator": "Filename Generator", - "VideoMetadataNode": "Update Video Metadata" } diff --git a/nodes/utils/control_panel.py b/nodes/utils/control_panel.py index 493501b..188b74f 100644 --- a/nodes/utils/control_panel.py +++ b/nodes/utils/control_panel.py @@ -5,7 +5,6 @@ Control Panel Node - Displays comprehensive workflow information from ..debug_utils import Logger -_overview_logger = Logger("ControlPanelOverview") _prompt_logger = Logger("ControlPanelPromptBreakdown") @@ -13,91 +12,6 @@ def _log_section(logger, title): logger.debug(f"\n{'=' * 60}\n{title}\n{'=' * 60}") -class ControlPanelOverview: - """ - A control panel node that displays all key information from the workflow. - This node acts as a central dashboard showing data from connected nodes. - Supports dynamic wildcard inputs that accept any data type. - """ - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - """ - Define input fields that will be displayed on the control panel. - Only accepts all_media_describe_data which contains all the workflow information. - """ - return { - "required": {}, - "optional": { - "all_media_describe_data": ("STRING", {"forceInput": True}), - }, - } - - RETURN_TYPES = () # No outputs - this is a display-only node - FUNCTION = "display_info" - CATEGORY = "Swiss Army Knife 🔪/Utils" - OUTPUT_NODE = True # Makes this an output node that displays in the UI - DESCRIPTION = ( - "Displays the full `all_media_describe_data` JSON inside ComfyUI’s Control Panel so you can audit structured describe " - "outputs without leaving the workflow." - ) - - def display_info(self, **kwargs): - """ - Display all the workflow information on the control panel. - Parses the all_media_describe_data JSON and returns structured data for UI display. - """ - import json - - _log_section(_overview_logger, "CONTROL PANEL - Workflow Information") - - all_media_data = kwargs.get("all_media_describe_data") - - _overview_logger.debug("\n🔍 DEBUG - All kwargs received:") - for key, value in kwargs.items(): - _overview_logger.debug( - f" - {key}: {type(value).__name__} = {str(value)[:200]}" - ) - - if all_media_data: - try: - # Parse the JSON data - if isinstance(all_media_data, str): - data = json.loads(all_media_data) - else: - data = all_media_data - - _overview_logger.debug("\n📊 All Media Describe Data:") - _overview_logger.debug(json.dumps(data, indent=2)) - - # DOUBLE the output - return BOTH in ui dict - result = { - "ui": { - "all_media_describe_data": [all_media_data], - "all_media_describe_data_copy": [all_media_data] # Duplicate for redundancy - } - } - - _overview_logger.debug("\n🔍 DEBUG - Returning to UI:") - _overview_logger.debug(json.dumps(result, indent=2)) - - return result - - except json.JSONDecodeError as e: - _overview_logger.error(f"Error parsing JSON: {e}") - _overview_logger.error(f"Raw data: {all_media_data}") - return {"ui": {"error": [f"Invalid JSON: {str(e)}"]}} - else: - _overview_logger.debug("\n(No all_media_describe_data connected)") - - _overview_logger.debug("\n" + "="*60 + "\n") - - return {"ui": {"status": ["No inputs connected"]}} - - class ControlPanelPromptBreakdown: """ A control panel node that displays prompt breakdown across 5 columns. @@ -215,12 +129,10 @@ class ControlPanelPromptBreakdown: # Export node class mapping CONTROL_PANEL_NODE_CLASS_MAPPINGS = { - "ControlPanelOverview": ControlPanelOverview, "ControlPanelPromptBreakdown": ControlPanelPromptBreakdown } # Export display name mapping CONTROL_PANEL_NODE_DISPLAY_NAME_MAPPINGS = { - "ControlPanelOverview": "🎛️ Control Panel Overview", "ControlPanelPromptBreakdown": "🎛️ Control Panel Prompt Breakdown" } diff --git a/web/docs/AzureStorageUpload.md b/web/docs/AzureStorageUpload.md index 0c1f800..d9fe550 100644 --- a/web/docs/AzureStorageUpload.md +++ b/web/docs/AzureStorageUpload.md @@ -12,7 +12,7 @@ Upload any generated file to Azure Blob Storage using the connection string stor ## Usage Tips 1. Set the Azure connection string under Swiss Army Knife settings and install `azure-storage-blob`. -2. Pass filenames from VideoMetadataNode or VHS nodes to push renders straight to the cloud. +2. Pass filenames from VHS nodes or any other file-producing node to push renders straight to the cloud. ## Additional Resources - [Full documentation](docs/nodes/azure-storage-upload/AZURE_STORAGE_UPLOAD.md) diff --git a/web/docs/CivitMetadataHelper.md b/web/docs/CivitMetadataHelper.md index c755be1..9f86398 100644 --- a/web/docs/CivitMetadataHelper.md +++ b/web/docs/CivitMetadataHelper.md @@ -13,7 +13,7 @@ Collect sampling settings, LoRA names, and prompts into formatted text/JSON that ## Usage Tips 1. Connect scheduler/LoRA/prompt values before uploading or archiving models. -2. Feed the formatted or JSON outputs into logging nodes, ShowText, or Azure uploads. +2. Feed the formatted or JSON outputs into logging nodes, ComfyUI's built-in text display nodes, or Azure uploads. ## Additional Resources - [Full documentation](docs/integrations/civitai/README.md) diff --git a/web/docs/ControlPanelOverview.md b/web/docs/ControlPanelOverview.md deleted file mode 100644 index 0367577..0000000 --- a/web/docs/ControlPanelOverview.md +++ /dev/null @@ -1,15 +0,0 @@ -# ControlPanelOverview -Dashboard-style output that renders the full `all_media_describe_data` JSON so you can audit everything Gemini/LM Studio produced. - -## Inputs -- Optional `all_media_describe_data` string (connect the LM Studio structured describe node's `json_output` here). - -## Outputs -- Display-only node that surfaces the JSON inside the UI's Control Panel tab. - -## Usage Tips -1. Drop the node anywhere in the workflow and connect your structured describe node's `json_output`. -2. Open the Control Panel sidebar to see structured cards for each section. - -## Additional Resources -- [Full documentation](docs/nodes/control-panel/README.md) diff --git a/web/docs/LoRAInfoExtractor.md b/web/docs/LoRAInfoExtractor.md index 9c3374e..24a41b1 100644 --- a/web/docs/LoRAInfoExtractor.md +++ b/web/docs/LoRAInfoExtractor.md @@ -16,8 +16,8 @@ Traverses Wan LoRA stacks, hashes every referenced file, optionally queries Civi ## Usage Tips - Store your CivitAI API key once in ComfyUI Settings → Swiss Army Knife and leave `use_civitai_api` enabled for rich metadata. -- Wire `lora_json` into the Video Metadata node to embed LoRA info back into rendered files. -- Display `lora_info` with `ShowText` or Control Panel to verify which LoRAs were active per render. +- Wire `lora_json` into your preferred metadata writer (FFmpeg scripts, Azure uploads, etc.) to embed LoRA info back into rendered files. +- Display `lora_info` with ComfyUI's built-in text display/output nodes or the Control Panel prompt widgets to verify which LoRAs were active per render. - If you only need hashes (offline), disable `use_civitai_api` for faster execution. ## Documentation diff --git a/web/docs/VideoMetadataNode.md b/web/docs/VideoMetadataNode.md deleted file mode 100644 index 61969ab..0000000 --- a/web/docs/VideoMetadataNode.md +++ /dev/null @@ -1,18 +0,0 @@ -# VideoMetadataNode -Append artist/comments/LoRA metadata to rendered videos using FFmpeg without re-encoding the stream. - -## Inputs -- `filenames` – VHS_VideoCombine output (VHS_FILENAMES). -- `comment` – multiline note that becomes a metadata tag. - -- `overwrite_original` – choose between in-place update or creating a `_metadata` copy. - -## Outputs -- `Filenames` – final list (matching VHS type) for downstream saves or uploads. - -## Usage Tips -1. Chain after VHS_VideoCombine. -2. Fill in LoRA/comment fields, run the node, then pass the resulting filenames to upload/archive steps. - -## Additional Resources -- [Full documentation](docs/nodes/video-metadata/VIDEO_METADATA.md) diff --git a/web/js/swiss-army-knife.js b/web/js/swiss-army-knife.js index 3e4d3bc..79d793f 100644 --- a/web/js/swiss-army-knife.js +++ b/web/js/swiss-army-knife.js @@ -101,551 +101,7 @@ app.registerExtension({ name: "comfyui_swissarmyknife.swiss_army_knife", async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Handle FilenameGenerator node - if (nodeData.name === "FilenameGenerator") { - debugLog("Registering FilenameGenerator node"); - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const result = onNodeCreated?.apply(this, arguments); - - // Add a preview widget to show the generated filename - this.addWidget( - "text", - "filename_preview", - "", - (value) => { - // This is a read-only preview widget - }, - { multiline: true } - ); - - // Function to update the filename preview - this.updateFilenamePreview = function () { - try { - // For scheduler, check if it's connected via input first - let scheduler = "scheduler_input"; - const schedulerInput = this.inputs?.find( - (input) => input.name === "scheduler" - ); - if (schedulerInput && schedulerInput.link) { - scheduler = "scheduler_connected"; - } else { - // Fallback to widget if no connection (shouldn't happen with SAMPLER type, but for safety) - const schedulerWidget = this.widgets.find( - (w) => w.name === "scheduler" - ); - if (schedulerWidget) { - scheduler = schedulerWidget.value || "scheduler_input"; - } - } - - const shift = this.widgets.find((w) => w.name === "shift")?.value || 12.0; - const total_steps = - this.widgets.find((w) => w.name === "total_steps")?.value || 40; - const shift_step = - this.widgets.find((w) => w.name === "shift_step")?.value || 25; - const high_cfg = - this.widgets.find((w) => w.name === "high_cfg")?.value || 3.0; - const low_cfg = - this.widgets.find((w) => w.name === "low_cfg")?.value || 4.0; - const base_filename = - this.widgets.find((w) => w.name === "base_filename")?.value || "base"; - const subdirectory_prefix = - this.widgets.find((w) => w.name === "subdirectory_prefix")?.value || ""; - const add_date_subdirectory = - this.widgets.find((w) => w.name === "add_date_subdirectory")?.value !== - false; - - // Format float values to replace decimal points with underscores - const shift_str = shift.toFixed(2).replace(".", "_"); - const high_cfg_str = high_cfg.toFixed(2).replace(".", "_"); - const low_cfg_str = low_cfg.toFixed(2).replace(".", "_"); - - // Clean scheduler string to ensure it's filename-safe - const scheduler_clean = scheduler - .toString() - .trim() - .replace(/\s/g, "_") - .toLowerCase(); - - // Clean base filename to ensure it's filename-safe - const base_clean = base_filename.toString().trim().replace(/\s/g, "_"); - - // Generate the filename components - const filename_parts = [ - base_clean, - "scheduler", - scheduler_clean, - "shift", - shift_str, - "total_steps", - total_steps.toString(), - "shift_step", - shift_step.toString(), - "highCFG", - high_cfg_str, - "lowCFG", - low_cfg_str, - ]; - - // Join all parts with underscores - let filename = filename_parts.join("_"); - - // Build directory structure with optional subdirectory prefix and date - const directory_parts = []; - - // Add subdirectory prefix if provided - if (subdirectory_prefix && subdirectory_prefix.trim()) { - const prefix_clean = subdirectory_prefix.trim().replace(/\s/g, "_"); - directory_parts.push(prefix_clean); - } - - // Add date subdirectory if requested - if (add_date_subdirectory) { - const current_date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format - directory_parts.push(current_date); - } - - // Combine directory parts with filename - let full_path; - if (directory_parts.length > 0) { - full_path = directory_parts.join("/") + "/" + filename; - } else { - full_path = filename; - } - - // Update the preview widget - const previewWidget = this.widgets.find( - (w) => w.name === "filename_preview" - ); - if (previewWidget) { - previewWidget.value = `Preview:\n${full_path}`; - } - } catch (error) { - console.error("%c[🔪SwissArmyKnife]", "color: #ef4444; font-weight: bold;", "Error updating filename preview:", error); - const previewWidget = this.widgets.find( - (w) => w.name === "filename_preview" - ); - if (previewWidget) { - previewWidget.value = "Preview:\nError generating filename"; - } - } - }; - - // Set up listeners for all input widgets to update the preview - const inputWidgetNames = [ - "shift", - "total_steps", - "shift_step", - "high_cfg", - "low_cfg", - "base_filename", - "subdirectory_prefix", - "add_date_subdirectory", - ]; - - for (const widgetName of inputWidgetNames) { - const widget = this.widgets.find((w) => w.name === widgetName); - if (widget) { - const originalCallback = widget.callback; - widget.callback = (...args) => { - if (originalCallback) { - originalCallback.apply(widget, args); - } - // Update preview after a small delay to ensure the value is set - setTimeout(() => this.updateFilenamePreview(), 10); - }; - } - } - - // Listen for input connections/disconnections to update preview - const originalOnConnectionsChange = this.onConnectionsChange; - this.onConnectionsChange = function (type, slotIndex, connected, linkInfo, ioSlot) { - if (originalOnConnectionsChange) { - originalOnConnectionsChange.call( - this, - type, - slotIndex, - connected, - linkInfo, - ioSlot - ); - } - // Update preview when scheduler input changes - if (type === 1 && ioSlot && ioSlot.name === "scheduler") { - // type 1 = input - setTimeout(() => this.updateFilenamePreview(), 10); - } - }; - - // Initial preview update - setTimeout(() => this.updateFilenamePreview(), 100); - - return result; - }; - } - - // Handle Control Panel node - else if (nodeData.name === "ControlPanelOverview") { - debugLog("Registering ControlPanelOverview node"); - - // On resize, keep DOM width sensible - const onResize = nodeType.prototype.onResize; - nodeType.prototype.onResize = function (size) { - const result = onResize?.call(this, size); - if (this._cp_dom) { - this._cp_dom.style.width = this.size[0] - 20 + "px"; - } - return result; - }; - - // Node creation - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const result = onNodeCreated?.apply(this, arguments); - - // Create a DOM widget area with three-column layout - if (!this._cp_dom) { - // Main container - const dom = document.createElement("div"); - dom.style.fontFamily = - "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace"; - dom.style.fontSize = "11px"; - dom.style.lineHeight = "1.35"; - dom.style.overflow = "auto"; - dom.style.maxHeight = "100%"; - dom.style.padding = "8px"; - dom.style.borderRadius = "6px"; - dom.style.background = "var(--comfy-menu-bg, #1e1e1e)"; - dom.style.border = "1px solid var(--border-color, #333)"; - dom.style.color = "var(--fg-color, #d4d4d4)"; - dom.style.display = "flex"; - dom.style.gap = "12px"; - - // Left column (final_prompt) - const leftColumn = document.createElement("div"); - leftColumn.style.flex = "1"; - leftColumn.style.minWidth = "0"; // Allow flex to shrink below content size - leftColumn.style.overflow = "auto"; - leftColumn.style.borderRight = "1px solid var(--border-color, #333)"; - leftColumn.style.paddingRight = "8px"; - leftColumn.style.display = "flex"; - leftColumn.style.flexDirection = "column"; - - const leftHeading = document.createElement("h3"); - leftHeading.textContent = "Final Prompt"; - leftHeading.style.margin = "0 0 8px 0"; - leftHeading.style.fontSize = "14px"; - leftHeading.style.fontWeight = "600"; - leftHeading.style.color = "var(--fg-color, #d4d4d4)"; - leftHeading.style.borderBottom = "2px solid var(--border-color, #333)"; - leftHeading.style.paddingBottom = "4px"; - - const leftContent = document.createElement("div"); - leftContent.style.whiteSpace = "pre-wrap"; - leftContent.style.wordBreak = "break-word"; - leftContent.style.overflow = "auto"; - leftContent.style.flex = "1"; - - leftColumn.appendChild(leftHeading); - leftColumn.appendChild(leftContent); - - // Middle column (gemini_status) - const middleColumn = document.createElement("div"); - middleColumn.style.flex = "1"; - middleColumn.style.minWidth = "0"; // Allow flex to shrink below content size - middleColumn.style.overflow = "auto"; - middleColumn.style.borderRight = "1px solid var(--border-color, #333)"; - middleColumn.style.paddingRight = "8px"; - middleColumn.style.display = "flex"; - middleColumn.style.flexDirection = "column"; - - const middleHeading = document.createElement("h3"); - middleHeading.textContent = "Gemini Status"; - middleHeading.style.margin = "0 0 8px 0"; - middleHeading.style.fontSize = "14px"; - middleHeading.style.fontWeight = "600"; - middleHeading.style.color = "var(--fg-color, #d4d4d4)"; - middleHeading.style.borderBottom = "2px solid var(--border-color, #333)"; - middleHeading.style.paddingBottom = "4px"; - - const middleContent = document.createElement("div"); - middleContent.style.whiteSpace = "pre-wrap"; - middleContent.style.wordBreak = "break-word"; - middleContent.style.overflow = "auto"; - middleContent.style.flex = "1"; - - middleColumn.appendChild(middleHeading); - middleColumn.appendChild(middleContent); - - // Right column (media_info, height, width) - const rightColumn = document.createElement("div"); - rightColumn.style.flex = "1"; - rightColumn.style.minWidth = "0"; // Allow flex to shrink below content size - rightColumn.style.overflow = "auto"; - rightColumn.style.display = "flex"; - rightColumn.style.flexDirection = "column"; - - const rightHeading = document.createElement("h3"); - rightHeading.textContent = "Media Info"; - rightHeading.style.margin = "0 0 8px 0"; - rightHeading.style.fontSize = "14px"; - rightHeading.style.fontWeight = "600"; - rightHeading.style.color = "var(--fg-color, #d4d4d4)"; - rightHeading.style.borderBottom = "2px solid var(--border-color, #333)"; - rightHeading.style.paddingBottom = "4px"; - - const rightContent = document.createElement("div"); - rightContent.style.whiteSpace = "pre-wrap"; - rightContent.style.wordBreak = "break-word"; - rightContent.style.overflow = "auto"; - rightContent.style.flex = "1"; - - rightColumn.appendChild(rightHeading); - rightColumn.appendChild(rightContent); - - dom.appendChild(leftColumn); - dom.appendChild(middleColumn); - dom.appendChild(rightColumn); - - // Add a DOM widget - const widget = this.addDOMWidget("ControlPanelOverview", "cp_display", dom, { - serialize: false, - hideOnZoom: false, - }); - - // Store references to content divs - this._cp_dom = dom; - this._cp_leftColumn = leftContent; - this._cp_middleColumn = middleContent; - this._cp_rightColumn = rightContent; - this._cp_widget = widget; - - // Initialize with waiting message - this._cp_leftColumn.textContent = "Awaiting execution..."; - this._cp_middleColumn.textContent = "Awaiting execution..."; - this._cp_rightColumn.textContent = "Awaiting execution..."; - } - - // Function to update with execution data - this.updateControlPanelData = function (data) { - debugLog("🔍 [ControlPanelOverview DEBUG] updateControlPanelData called"); - debugLog("🔍 [ControlPanelOverview DEBUG] data received:", data); - debugLog("🔍 [ControlPanelOverview DEBUG] data keys:", Object.keys(data || {})); - - if ( - !this._cp_leftColumn || - !this._cp_middleColumn || - !this._cp_rightColumn || - !data - ) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Missing columns or data, returning" - ); - return; - } - - debugLog("[ControlPanelOverview] Received data:", data); - - // Extract all_media_describe_data - TRY MULTIPLE SOURCES - let mediaData = null; - let rawData = null; - - // Try all possible locations for the data - if (data.all_media_describe_data) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Found data.all_media_describe_data" - ); - rawData = Array.isArray(data.all_media_describe_data) - ? data.all_media_describe_data[0] - : data.all_media_describe_data; - } else if (data.all_media_describe_data_copy) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Found data.all_media_describe_data_copy" - ); - rawData = Array.isArray(data.all_media_describe_data_copy) - ? data.all_media_describe_data_copy[0] - : data.all_media_describe_data_copy; - } else { - debugLog( - "🔍 [ControlPanelOverview DEBUG] No all_media_describe_data found in:", - Object.keys(data) - ); - } - - debugLog("🔍 [ControlPanelOverview DEBUG] rawData:", rawData); - debugLog("🔍 [ControlPanelOverview DEBUG] rawData type:", typeof rawData); - - if (rawData) { - try { - // Parse JSON if it's a string - mediaData = typeof rawData === "string" ? JSON.parse(rawData) : rawData; - debugLog( - "🔍 [ControlPanelOverview DEBUG] Parsed mediaData:", - mediaData - ); - debugLog("[ControlPanelOverview] Parsed media data:", mediaData); - } catch (e) { - console.error("🔍 [ControlPanelOverview DEBUG] Error parsing JSON:", e); - debugLog("[ControlPanelOverview] Error parsing JSON:", e); - this._cp_leftColumn.textContent = "Error parsing data"; - this._cp_middleColumn.textContent = "Error parsing data"; - this._cp_rightColumn.textContent = "Error parsing data"; - return; - } - } - - if (!mediaData) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] No mediaData, showing 'no data available'" - ); - this._cp_leftColumn.textContent = "(No data available)"; - this._cp_middleColumn.textContent = "(No data available)"; - this._cp_rightColumn.textContent = "(No data available)"; - return; - } - - debugLog( - "🔍 [ControlPanelOverview DEBUG] mediaData keys:", - Object.keys(mediaData) - ); - - // Helper function to format field display - const formatValue = (value, maxLength = 500) => { - let valueStr = String(value); - if (valueStr.length > maxLength) { - valueStr = valueStr.substring(0, maxLength) + "... (truncated)"; - } - return valueStr; - }; - - // Left column: Positive Prompt (check both field names for backwards compatibility) - const finalText = - mediaData.positive_prompt || - mediaData.final_prompt || - mediaData.description; - if (finalText) { - debugLog( - "🔍 [ControlPanel DEBUG] Setting positive prompt:", - finalText.substring(0, 50) - ); - this._cp_leftColumn.textContent = formatValue(finalText, 2000); - } else { - debugLog( - "🔍 [ControlPanelOverview DEBUG] No positive_prompt/final_prompt/description in mediaData" - ); - this._cp_leftColumn.textContent = "(No final text in data)"; - } - - // Middle column: Gemini Status - if (mediaData.gemini_status) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Setting gemini_status:", - mediaData.gemini_status.substring(0, 50) - ); - this._cp_middleColumn.textContent = formatValue( - mediaData.gemini_status, - 2000 - ); - } else { - debugLog( - "🔍 [ControlPanelOverview DEBUG] No gemini_status in mediaData" - ); - this._cp_middleColumn.textContent = "(No gemini_status in data)"; - } - - // Right column: Media Info (including height, width, other metadata) - const rightLines = []; - - if (mediaData.media_info) { - rightLines.push(`📊 Media Info:\n${formatValue(mediaData.media_info)}\n`); - } - - if (mediaData.height !== undefined) { - rightLines.push(`📐 Height: ${mediaData.height}\n`); - } - - if (mediaData.width !== undefined) { - rightLines.push(`📐 Width: ${mediaData.width}\n`); - } - - // Stop here - only show media_info, height, and width - // Removed loop that displayed additional fields (subject, cinematic_aesthetic, etc.) - - if (rightLines.length === 0) { - debugLog("🔍 [ControlPanelOverview DEBUG] No right column data"); - this._cp_rightColumn.textContent = "(No media info in data)"; - } else { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Setting right column with", - rightLines.length, - "lines" - ); - this._cp_rightColumn.textContent = rightLines.join("\n"); - } - - debugLog("🔍 [ControlPanelOverview DEBUG] Display update complete"); - debugLog("[ControlPanelOverview] Updated display with execution data"); - }; - - // Add onExecuted handler to update display with execution results - const originalOnExecuted = this.onExecuted; - this.onExecuted = function (message) { - debugLog("🔍 [ControlPanelOverview DEBUG] onExecuted called"); - debugLog("🔍 [ControlPanelOverview DEBUG] Full message object:", message); - debugLog( - "🔍 [ControlPanelOverview DEBUG] message keys:", - Object.keys(message || {}) - ); - - if (message) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] message.output:", - message.output - ); - if (message.output) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] message.output keys:", - Object.keys(message.output) - ); - } - } - - debugLog("[ControlPanelOverview] onExecuted called with message:", message); - - // Try multiple data sources - data might be at root or in output - if (message && message.output) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Calling updateControlPanelData with message.output:", - message.output - ); - this.updateControlPanelData(message.output); - } else if ( - message && - (message.all_media_describe_data || message.all_media_describe_data_copy) - ) { - debugLog( - "🔍 [ControlPanelOverview DEBUG] Calling updateControlPanelData with message (root level):", - message - ); - this.updateControlPanelData(message); - } else { - debugLog( - "🔍 [ControlPanelOverview DEBUG] No data found in message.output or message root" - ); - } - - // Call original onExecuted if it exists - return originalOnExecuted?.call(this, message); - }; - - return result; - }; - } - - // Handle ControlPanelPromptBreakdown node - else if (nodeData.name === "ControlPanelPromptBreakdown") { + if (nodeData.name === "ControlPanelPromptBreakdown") { debugLog("Registering ControlPanelPromptBreakdown node"); // On resize, keep DOM width sensible