Add files via upload

This commit is contained in:
A.I Joe
2026-01-18 12:18:01 +01:00
committed by GitHub
parent b4e4ec5af8
commit 7a235b83b1
4 changed files with 506 additions and 1 deletions
+182 -1
View File
@@ -1 +1,182 @@
# ComfyUI-Metadata-Extractor
# ComfyUI Metadata Extractor
A powerful custom node for ComfyUI that extracts metadata from images, videos, and audio files generated by ComfyUI.
## Features
✅ **Multiple File Format Support:**
- **Images:** PNG, JPEG
- **Videos:** MP4, AVI, MOV, MKV, WebM
- **Audio:** FLAC, MP3, WAV, OGG
✅ **Comprehensive Metadata Extraction:**
- Positive prompt
- Negative prompt
- Seed
- Steps
- CFG scale
- Sampler name
- Scheduler
- Width/Height/Dimensions
- Batch size
- FPS (for videos)
- Full workflow JSON
✅ **Two Node Variants:**
1. **MetadataExtractor (File)** - Works with file paths
2. **ImageMetadataExtractor (Image)** - Works with IMAGE tensor inputs
## Installation
### Method 1: Manual Installation
1. Navigate to your ComfyUI custom_nodes directory:
```bash
cd ComfyUI/custom_nodes/
```
2. Create a new folder for the node:
```bash
mkdir ComfyUI-Metadata-Extractor
cd ComfyUI-Metadata-Extractor
```
3. Copy these files into the folder:
- `__init__.py`
- `metadata_extractor_node.py`
4. Install required dependencies:
```bash
pip install pillow mutagen
```
5. Restart ComfyUI
### Method 2: Git Clone (if you create a repo)
```bash
cd ComfyUI/custom_nodes/
git clone https://github.com/yourusername/ComfyUI-Metadata-Extractor.git
cd ComfyUI-Metadata-Extractor
pip install -r requirements.txt
```
## Usage
### MetadataExtractor (File Path)
1. Add the **"Extract Metadata (File)"** node to your workflow
2. Connect a file path string to the `file_path` input
3. All metadata outputs will be available as separate connections:
- `positive_prompt` (STRING)
- `negative_prompt` (STRING)
- `seed` (STRING)
- `steps` (STRING)
- `width` (INT)
- `height` (INT)
- `batch_size` (INT)
- `cfg` (FLOAT)
- `sampler_name` (STRING)
- `scheduler` (STRING)
- `full_workflow_json` (STRING)
### ImageMetadataExtractor (Image Input)
1. Add the **"Extract Metadata (Image)"** node to your workflow
2. Connect an IMAGE output to the `image` input
3. Optionally provide the source file path for full metadata extraction
4. Outputs:
- `prompt` (STRING)
- `negative_prompt` (STRING)
- `width` (INT)
- `height` (INT)
- `metadata_json` (STRING)
## Example Workflows
### Example 1: Extract and Reuse Prompts
Load an image → Extract Metadata (File) → Connect `positive_prompt` to CLIPTextEncode → Generate similar image
### Example 2: Batch Processing
Load multiple images → Extract Metadata → Save prompts to text files → Analyze generation patterns
### Example 3: Video Analysis
Load video path → Extract Metadata (File) → Get FPS, dimensions, and generation settings
## Technical Details
### How It Works
**For PNG Images:**
- Reads metadata from PNG `info` dictionary
- Extracts `workflow` and `prompt` JSON data
- Parses node structure to find generation parameters
**For Videos:**
- Uses ffprobe to read video metadata
- Looks for ComfyUI metadata in format tags
- Extracts dimensions and FPS from stream info
**For Audio:**
- Uses mutagen library to read audio tags
- Searches for JSON metadata in comment/description fields
- Preserves generation parameters
### Metadata Structure
ComfyUI stores metadata in a node-based JSON structure. This node intelligently parses:
- KSampler nodes for sampling parameters
- CLIPTextEncode nodes for prompts
- EmptyLatentImage nodes for dimensions
- Custom metadata fields
## Dependencies
- Python 3.8+
- PIL/Pillow (image handling)
- mutagen (audio metadata)
- ffprobe (video metadata, optional but recommended)
## Troubleshooting
**"No metadata found"**
- Ensure the file was generated by ComfyUI with metadata saving enabled
- Check that the file hasn't been re-encoded or processed by other tools
**Video metadata extraction fails**
- Install ffmpeg/ffprobe: `apt-get install ffmpeg` or download from ffmpeg.org
- Ensure ffprobe is in your system PATH
**Audio metadata extraction fails**
- Install mutagen: `pip install mutagen`
- Some audio formats may not preserve metadata after encoding
## Limitations
- Only extracts metadata that was embedded during generation
- Images downloaded from web sources typically have metadata stripped
- Video metadata preservation depends on the encoder settings
- Some file formats have limited metadata support
## Future Enhancements
- [ ] Support for more video formats
- [ ] Batch processing multiple files
- [ ] Export metadata to CSV/JSON files
- [ ] Metadata comparison between files
- [ ] Automatic prompt reconstruction from partial data
## Contributing
Feel free to submit issues, feature requests, or pull requests!
## License
MIT License - Free to use and modify
## Credits
Created for the ComfyUI community. Special thanks to the ComfyUI developers and the AI art community.
+8
View File
@@ -0,0 +1,8 @@
"""
ComfyUI Metadata Extractor
Extracts metadata from PNG images and MP4/video files generated by ComfyUI
"""
from .metadata_extractor import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+314
View File
@@ -0,0 +1,314 @@
"""
ComfyUI Metadata Extractor - Improved Version
Extracts metadata from PNG images and MP4/video files
Now properly extracts: prompts, seed, steps, cfg, sampler, scheduler
"""
import json
import os
import subprocess
import folder_paths
try:
from PIL import Image
except ImportError:
import PIL.Image as Image
class MetadataExtractorImproved:
"""
Extracts metadata from ComfyUI-generated files
Properly parses seed, steps, cfg from the prompt structure
"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.input_dir = folder_paths.get_input_directory()
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = []
try:
files = [f for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f)) and
f.lower().endswith(('.png', '.jpg', '.jpeg', '.mp4', '.avi', '.mov', '.mkv', '.webm'))]
except:
pass
return {
"required": {},
"optional": {
"video": ("IMAGE",),
"file_path": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Full path to file"
}),
"filename": ([""] + sorted(files), {
"default": ""
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "FLOAT", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("positive_prompt", "negative_prompt", "seed", "steps", "cfg", "sampler", "scheduler", "metadata_json", "file_info")
FUNCTION = "extract"
CATEGORY = "utils/metadata"
OUTPUT_NODE = False
def find_video_file_in_workflow(self, video_tensor):
"""Try to find source video file"""
possible_locations = [self.input_dir, self.output_dir]
video_files = []
for location in possible_locations:
if not os.path.exists(location):
continue
try:
for f in os.listdir(location):
if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
full_path = os.path.join(location, f)
video_files.append((full_path, os.path.getmtime(full_path)))
except:
pass
video_files.sort(key=lambda x: x[1], reverse=True)
if video_files:
print(f"[Metadata] 💡 Found: {os.path.basename(video_files[0][0])}")
return video_files[0][0]
return None
def extract_video_metadata(self, file_path):
"""Extract metadata from video files using ffprobe"""
try:
cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json',
'-show_format', '-show_streams', file_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return None
video_info = json.loads(result.stdout)
tags = video_info.get('format', {}).get('tags', {})
print(f"[Metadata] Video tags: {list(tags.keys())}")
metadata = {}
metadata_fields = ['comment', 'Comment', 'description', 'Description',
'workflow', 'Workflow', 'prompt', 'Prompt']
for field in metadata_fields:
if field in tags:
try:
parsed = json.loads(tags[field])
metadata[field.lower()] = parsed
print(f"[Metadata] ✅ Found '{field}' tag")
except:
metadata[field.lower()] = tags[field]
# Get video properties
for stream in video_info.get('streams', []):
if stream.get('codec_type') == 'video':
metadata['video_width'] = stream.get('width', 0)
metadata['video_height'] = stream.get('height', 0)
fps_str = stream.get('r_frame_rate', '0/1')
try:
fps_num, fps_den = map(int, fps_str.split('/'))
metadata['video_fps'] = fps_num / fps_den if fps_den else 0
except:
metadata['video_fps'] = 0
metadata['video_duration'] = float(stream.get('duration', 0))
break
return metadata if metadata else None
except FileNotFoundError:
print("[Metadata] ❌ ffprobe not found! Install ffmpeg")
return None
except Exception as e:
print(f"[Metadata] Error: {e}")
return None
def extract_png_metadata(self, file_path):
"""Extract metadata from PNG files"""
try:
img = Image.open(file_path)
metadata = {}
if hasattr(img, 'info'):
if 'workflow' in img.info:
try:
metadata['workflow'] = json.loads(img.info['workflow'])
except:
pass
if 'prompt' in img.info:
try:
metadata['prompt'] = json.loads(img.info['prompt'])
except:
pass
return metadata if metadata else None
except Exception as e:
print(f"[Metadata] Error reading PNG: {e}")
return None
def parse_comfyui_metadata(self, metadata):
"""Parse metadata and extract ALL parameters"""
result = {
'positive_prompt': "",
'negative_prompt': "",
'seed': 0,
'steps': 0,
'cfg': 0.0,
'sampler': "",
'scheduler': "",
'metadata_json': "{}",
'file_info': ""
}
try:
# Get prompt data (node-based structure)
prompt_data = None
if 'prompt' in metadata:
prompt_data = metadata['prompt']
elif 'comment' in metadata and isinstance(metadata['comment'], dict):
prompt_data = metadata['comment']
if not prompt_data:
result['metadata_json'] = json.dumps(metadata, indent=2)
return result
print(f"[Metadata] Parsing {len(prompt_data)} nodes...")
# Track what we've found
positive_found = False
negative_found = False
# Iterate through all nodes
for node_id, node_data in prompt_data.items():
if not isinstance(node_data, dict):
continue
inputs = node_data.get('inputs', {})
class_type = node_data.get('class_type', '')
# Extract prompts
if class_type in ['CLIPTextEncode', 'CLIPTextEncodeSDXL', 'CLIPTextEncodeFlux']:
text_content = inputs.get('text', '')
if text_content:
if not positive_found:
result['positive_prompt'] = text_content
positive_found = True
print(f"[Metadata] ✅ Positive prompt")
elif not negative_found:
result['negative_prompt'] = text_content
negative_found = True
print(f"[Metadata] ✅ Negative prompt")
# Extract seed from RandomNoise node
if class_type == 'RandomNoise':
if 'noise_seed' in inputs:
result['seed'] = int(inputs['noise_seed'])
print(f"[Metadata] ✅ Seed: {result['seed']}")
elif isinstance(inputs.get('noise_seed'), list):
# Handle array format: ["get", ["123", 0]]
try:
result['seed'] = int(inputs['noise_seed'][0])
except:
pass
# Extract sampler
if class_type == 'KSamplerSelect':
sampler_name = inputs.get('sampler_name', '')
if sampler_name:
result['sampler'] = sampler_name
print(f"[Metadata] ✅ Sampler: {sampler_name}")
# Extract steps and cfg from scheduler nodes
if class_type in ['LTXVScheduler', 'BasicScheduler', 'KScheduler']:
if 'steps' in inputs:
result['steps'] = int(inputs['steps'])
print(f"[Metadata] ✅ Steps: {result['steps']}")
# Extract CFG from guider nodes
if class_type in ['CFGGuider', 'DualCFGGuider']:
if 'cfg' in inputs:
result['cfg'] = float(inputs['cfg'])
print(f"[Metadata] ✅ CFG: {result['cfg']}")
# Store full metadata
result['metadata_json'] = json.dumps(metadata, indent=2)
# Create file info
info_parts = []
if 'video_width' in metadata:
info_parts.append(f"{metadata['video_width']}x{metadata['video_height']}")
if 'video_fps' in metadata:
info_parts.append(f"{metadata['video_fps']:.2f} FPS")
if 'video_duration' in metadata:
info_parts.append(f"{metadata['video_duration']:.2f}s")
result['file_info'] = " | ".join(info_parts) if info_parts else "PNG Image"
except Exception as e:
print(f"[Metadata] Error parsing: {e}")
import traceback
traceback.print_exc()
return result
def extract(self, video=None, file_path="", filename=""):
"""Main extraction function"""
actual_path = None
if file_path and os.path.exists(file_path):
actual_path = file_path
elif filename:
actual_path = os.path.join(self.input_dir, filename)
elif video is not None:
actual_path = self.find_video_file_in_workflow(video)
if not actual_path or not os.path.exists(actual_path):
return ("", "", 0, 0, 0.0, "", "", "{}", "File not found")
print(f"[Metadata] 📂 Processing: {os.path.basename(actual_path)}")
ext = os.path.splitext(actual_path)[1].lower()
metadata = None
if ext in ['.png', '.jpg', '.jpeg']:
metadata = self.extract_png_metadata(actual_path)
elif ext in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
metadata = self.extract_video_metadata(actual_path)
else:
return ("", "", 0, 0, 0.0, "", "", "{}", f"Unsupported: {ext}")
if not metadata:
return ("", "", 0, 0, 0.0, "", "", "{}", "No metadata found")
result = self.parse_comfyui_metadata(metadata)
return (
result['positive_prompt'],
result['negative_prompt'],
result['seed'],
result['steps'],
result['cfg'],
result['sampler'],
result['scheduler'],
result['metadata_json'],
result['file_info']
)
NODE_CLASS_MAPPINGS = {
"MetadataExtractorImproved": MetadataExtractorImproved
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MetadataExtractorImproved": "📹 Extract Metadata (PNG/Video)"
}
+2
View File
@@ -0,0 +1,2 @@
Pillow>=9.0.0
mutagen>=1.45.0