Files
EricRollei-AAA_Metadata_System/documentation/node_integration_guide.md
T

767 lines
22 KiB
Markdown

# Metadata System Integration Guide (PREVIEW)
## 1. Introduction
This guide explains how to integrate your custom nodes with the new metadata system. The system allows nodes to read and write metadata to multiple storage formats while ensuring proper data structure and compatibility.
> **Note**: This is a preview guide and may evolve as we finalize testing of the system.
## 2. System Overview
The metadata system uses a service-based architecture to provide a unified interface for handling metadata:
- **MetadataService**: Central façade that coordinates operations across different handlers
- **Handlers**: Specialized components for different storage formats (embedded, XMP, text, database)
- **Utilities**: Namespace registration, format detection, error handling, etc.
## 3. Integration Steps
### 3.1 Basic Integration
To integrate your node with the metadata system:
```python
# 1. Import the service
from eric_metadata.service import MetadataService
class MyAnalysisNode:
"""My analysis node with metadata support"""
def __init__(self):
# Initialize the metadata service
self.metadata_service = MetadataService(debug=False)
def process(self, input_image, input_path):
"""Process input image and update metadata
Args:
input_image: Image to process
input_path: Path to the image file
"""
# Use the metadata service here
# ...
```
### 3.2 Using the Resource Identifier
The resource identifier is critically important for XMP sidecar files. It ensures all nodes use the same identifier for the same file:
```python
# Always set the resource identifier before writing metadata
filename = os.path.basename(input_filepath)
resource_uri = f"file:///{filename}"
self.metadata_service.set_resource_identifier(resource_uri)
```
### 3.3 Reading Existing Metadata
To read metadata from an image:
```python
# Read from preferred source with fallback to others
metadata = self.metadata_service.read_metadata(input_filepath, source='xmp', fallback=True)
# Access specific sections
if 'analysis' in metadata and 'blur' in metadata['analysis']:
blur_score = metadata['analysis']['blur'].get('score', 0.0)
```
### 3.4 Merging Metadata
The system automatically merges new metadata with existing data. If you need to merge manually:
```python
# Read existing metadata
existing = self.metadata_service.read_metadata(filepath)
# Create your new metadata
new_metadata = {'analysis': {'my_feature': {'score': 0.75}}}
# Merge them (this happens automatically in write_metadata)
merged = self.metadata_service.merge_metadata(filepath, new_metadata)
```
## 4. Metadata Structure Guidelines
### 4.1 Top-Level Organization
```python
metadata = {
'basic': {}, # Title, description, keywords, etc.
'regions': {} # Face regions, subject areas, etc.
}
```
### 4.2 Basic Metadata
```python
basic_data = {
'title': 'My Image Title',
'rating': 4 # 0-5 scale
}
metadata['basic'] = basic_data
```
### 4.3 Analysis Data
For measurements and scores:
```python
analysis_data = {
'technical': {
'blur': {
'score': 0.85,
'higher_better': False,
'timestamp': '2023-12-01T12:34:56'
},
'noise': {
'score': 0.12,
'higher_better': False
}
}
}
metadata['analysis'] = analysis_data
```
### 4.4 AI Generation Data
```python
ai_data = {
'generation': {
'model': 'sd_xl_base_1.0',
'prompt': 'a photo of a cat',
'negative_prompt': 'blurry, low quality',
'seed': 42,
'steps': 30,
'cfg_scale': 7.5,
'sampler': 'euler_a',
'timestamp': datetime.now().isoformat()
}
}
metadata['ai_info'] = ai_data
```
## 5. Advanced Integration
### 5.1 Node Types
Different node types benefit from different integration patterns:
#### Analysis Nodes
```python
class ImageAnalysisNode:
def __init__(self):
self.metadata_service = MetadataService(debug=False)
def analyze(self, image_path):
# Run your analysis
sharpness_score = self._calculate_sharpness(image_path)
# Prepare metadata
metadata = {
'analysis': {
'technical': {
'sharpness': {
'score': sharpness_score,
'higher_better': True,
'timestamp': datetime.now().isoformat()
}
}
}
}
# Write metadata
self.metadata_service.write_metadata(
image_path,
metadata,
targets=['embedded', 'xmp']
)
return sharpness_score
```
#### Generation Nodes
```python
class MyGenerationNode:
def __init__(self):
self.metadata_service = MetadataService(debug=False)
def generate_image(self, model_name, prompt, seed):
# Generate image (implementation specific)
image, path = self._generate_image(model_name, prompt, seed)
# Prepare generation metadata
metadata = {
'ai_info': {
'generation': {
'model': model_name,
'prompt': prompt,
'seed': seed,
'timestamp': datetime.now().isoformat()
}
}
}
# Write metadata
self.metadata_service.write_metadata(path, metadata)
return image, path
```
### 5.2 Format-Specific Options
Configure handler-specific behavior:
```python
# Configure specific handler options
self.metadata_service.configure_handler(
'embedded',
preserve_workflow_data=True, # PNG workflow preservation
use_exiftool=True # Force ExifTool for certain formats
)
# Configure XMP handler options
self.metadata_service.configure_handler(
'xmp',
create_sidecar_for_raw=True, # Always create XMP sidecar for RAW files
merge_strategy='newest' # How to handle conflicts
)
# Configure database handler
self.metadata_service.configure_handler(
'database',
db_path='/path/to/metadata.db',
auto_index_keywords=True
)
```
### 5.3 Working With Regions
For facial recognition or object detection nodes:
```python
def process_faces(self, image_path, faces):
# Create regions metadata
regions_metadata = {
'regions': {
'faces': [],
'summary': {
'face_count': len(faces),
'detector_type': 'my_face_detector_v1'
}
}
}
# Build face entries
for idx, face in enumerate(faces):
face_data = {
'type': 'Face',
'name': f'Face {idx+1}',
'area': {
'x': face['x'] / img_width, # Normalized coordinates (0-1)
'y': face['y'] / img_height,
'w': face['width'] / img_width,
'h': face['height'] / img_height
},
'extensions': {
'eiqa': {
'face_analysis': {
'confidence': face['confidence'],
'age': face.get('age', 0),
'gender': face.get('gender', 'unknown'),
'emotion': {
'scores': face.get('emotions', {})
}
}
}
}
}
regions_metadata['regions']['faces'].append(face_data)
# Write metadata
self.metadata_service.write_metadata(image_path, regions_metadata)
```
### 5.4 Example for XMP Sidecar Handler
```python
from eric_metadata.handlers.xmp import XMPSidecarHandler
def create_separate_xmp_file(self, image_path, metadata):
# Using the handler directly gives more control over specific XMP features
# that aren't exposed through the general service interface
xmp_handler = XMPSidecarHandler(debug=False)
# Set resource identifier
filename = os.path.basename(image_path)
xmp_handler.set_resource_identifier(f"file:///{filename}")
# Write XMP sidecar file
return xmp_handler.write_metadata(image_path, metadata)
```
## 6. Error Handling
### 6.1 Graceful Error Handling
```python
def safe_read_metadata(self, filepath):
try:
metadata = self.metadata_service.read_metadata(filepath, fallback=True)
return metadata
except Exception as e:
print(f"Error reading metadata: {e}")
# Return empty structure as fallback
return {'basic': {}, 'analysis': {}, 'ai_info': {}, 'regions': {}}
```
### 6.2 Format Recovery
```python
# Try embedded first, with fallback to others if it fails
metadata = self.metadata_service.read_metadata(
filepath,
source='embedded',
fallback=True,
fallback_priority=['xmp', 'database']
)
# Write with fallback options
success = self.metadata_service.write_metadata(
filepath,
metadata,
targets=['embedded'],
fallback_targets=['xmp'] # If embedded fails, try XMP
)
```
## 7. Advanced Metadata Patterns
### 7.1 Handling Batched Images
```python
def process_batch(self, image_paths):
results = {}
# Use batch operations for efficiency
metadata_map = self.metadata_service.batch_read(image_paths)
for path, metadata in metadata_map.items():
# Process each image's metadata
# ...
# Update with new data
new_metadata = self._generate_metadata(path)
merged = self.metadata_service.merge_metadata(metadata, new_metadata)
results[path] = merged
# Write back in batch
self.metadata_service.batch_write(results)
return results
```
```python
# For very large batches, consider processing in chunks to manage memory usage
max_chunk_size = 100
for i in range(0, len(large_file_list), max_chunk_size):
chunk = large_file_list[i:i + max_chunk_size]
self.metadata_service.batch_operation('read', chunk)
```
### 7.2 Using Specific Handlers Directly
```python
from eric_metadata.handlers.db import DatabaseHandler
# Direct database queries for advanced needs
def find_images_by_criteria(self, criteria):
# Create database handler directly
db_handler = DatabaseHandler(debug=False)
# Build query
query = {
'scores': [
# category, metric, operator, value
('technical', 'blur.score', '>', 0.8)
],
'keywords': ['portrait', 'outdoor'],
'classifications': [('style', 'cinematic')],
'orientation': 'landscape',
'order_by': 'i.rating DESC',
'limit': 50
}
# Execute search
results = db_handler.search_images(query)
return results
```
### 7.3 Custom Metadata Extensions
```python
# Create custom metadata sections
metadata = {
'my_extension': {
'custom_field1': 'value1',
'measurements': {
'value': 42.5,
'units': 'pixels',
'confidence': 0.95
}
}
}
# Configure the service to register your extension namespace
self.metadata_service.register_namespace(
'my_ext',
'http://example.org/myextension/'
)
# Write with your extension
self.metadata_service.write_metadata(filepath, metadata)
```
## 8. Performance Considerations
### 8.1 Resource Management
```python
# Use context manager for clean resource handling
with MetadataService() as metadata_service:
metadata = metadata_service.read_metadata(filepath)
# Process metadata...
metadata_service.write_metadata(filepath, metadata)
# Resources automatically cleaned up
```
### 8.2 Optimizing for Large Batches
```python
# For large batches, configure connection pooling
self.metadata_service.configure_handler(
'database',
connection_pooling=True,
max_connections=5
)
# Perform operations in chunks
for chunk in self._chunks(large_file_list, 100):
self.metadata_service.batch_operation('read', chunk)
```
### 8.3 Minimizing Size for Embedding
```python
def prepare_compact_metadata(self, full_metadata):
# Create compact version for embedding
compact = {}
# Include only essential fields
if 'basic' in full_metadata:
compact['basic'] = {
'title': full_metadata['basic'].get('title'),
'keywords': full_metadata['basic'].get('keywords')
}
if 'ai_info' in full_metadata:
# Just include critical generation parameters
gen_data = full_metadata['ai_info'].get('generation', {})
compact['ai_info'] = {
'generation': {
'model': gen_data.get('model'),
'seed': gen_data.get('seed'),
'sampler': gen_data.get('sampler'),
'steps': gen_data.get('steps')
}
}
return compact
```
### 8.4 Thread Safety
When multiple nodes might be accessing the metadata service concurrently:
```python
# The metadata service's write operations are internally thread-safe
# but for complex operations, consider using locks
import threading
class ThreadSafeNode:
def __init__(self):
self.metadata_service = MetadataService()
self._lock = threading.Lock()
def process_batch(self, items):
with self._lock:
# Perform a sequence of read/modify/write operations atomically
results = []
for item in items:
metadata = self.metadata_service.read_metadata(item)
# modify metadata
self.metadata_service.write_metadata(item, metadata)
results.append(metadata)
return results
```
## 9. Complete Working Example
Here's an example of a complete node integration:
```python
import os
from datetime import datetime
from eric_metadata.service import MetadataService
class ImageEnhancementNode:
"""Node that enhances images and records metadata about the process"""
def __init__(self):
self.metadata_service = MetadataService(debug=False)
self.metadata_service.configure_handler('embedded', preserve_workflow_data=True)
def enhance_image(self, input_filepath, enhancement_type, strength):
"""Enhance image and record metadata about the enhancement"""
# 1. Read existing metadata (if any)
existing_metadata = self.metadata_service.read_metadata(
input_filepath,
fallback=True
)
# 2. Process the image (implementation-specific)
output_path = self._enhance_image(input_filepath, enhancement_type, strength)
# 3. Set resource identifier (important for XMP)
filename = os.path.basename(output_path)
resource_uri = f"file:///{filename}"
self.metadata_service.set_resource_identifier(resource_uri)
# 4. Create new metadata about the enhancement
enhancement_metadata = {
'basic': {
'title': f"{enhancement_type.capitalize()} Enhanced Image",
'description': f"Enhanced using {enhancement_type} algorithm with strength {strength}",
'keywords': [enhancement_type, 'enhanced', 'processed']
},
'analysis': {
'processing': {
'enhancement_type': enhancement_type,
'strength': strength,
'quality_estimate': self._estimate_quality(output_path),
'timestamp': datetime.now().isoformat()
}
},
'workflow_info': {
'node_type': 'ImageEnhancement',
'version': '1.0.0',
'processing_time_ms': self._last_processing_time
}
}
# 5. Write metadata to all targets
self.metadata_service.write_metadata(
output_path,
enhancement_metadata,
targets=['embedded', 'xmp']
)
# 6. Return the enhanced image path
return output_path
def _enhance_image(self, input_path, enhancement_type, strength):
"""Implementation of actual enhancement"""
# Actual enhancement code here...
output_path = f"{os.path.splitext(input_path)[0]}_enhanced.png"
self._last_processing_time = 1250 # Example processing time in ms
return output_path
def _estimate_quality(self, image_path):
"""Estimate quality of enhanced output"""
# Quality estimation code here...
return 0.85 # Example quality score
```
## 10. Common Use Cases
### 10.1 Image Analysis Node
```python
def analyze_and_tag(self, image_path):
"""Analyze image content and add metadata tags"""
# Run image analysis (implementation-specific)
analysis_results = self._analyze_content(image_path)
# Extract tags from analysis
tags = [item['label'] for item in analysis_results
if item['confidence'] > 0.7]
# Build confidence scores dictionary
confidence_scores = {
item['label']: item['confidence']
for item in analysis_results
}
# Create metadata
metadata = {
'basic': {
'keywords': tags
},
'analysis': {
'classification': {
'tags': tags,
'confidence_scores': confidence_scores,
'detector': 'my_classifier_v1'
}
}
}
# Write metadata
self.metadata_service.write_metadata(
image_path, metadata, targets=['embedded', 'xmp', 'database']
)
# Return analysis results and tags
return analysis_results, tags
```
### 10.2 Copyright and Attribution Node
```python
def add_copyright_info(self, image_path, creator, rights_usage, copyright_text):
"""Add copyright and attribution information"""
# Format standard copyright statement
if not copyright_text:
current_year = datetime.now().year
copyright_text = f"© {current_year} {creator}. All rights reserved."
# Create metadata
metadata = {
'basic': {
'creator': creator,
'copyright': copyright_text,
'rights': rights_usage
}
}
# Add additional XMP Rights fields
metadata['xmp_rights'] = {
'Marked': True,
'UsageTerms': rights_usage or copyright_text,
'WebStatement': f"https://example.com/rights/{creator}"
}
# Write metadata with priority to embedded
self.metadata_service.write_metadata(
image_path, metadata, targets=['embedded', 'xmp']
)
return copyright_text
```
### 10.3 Workflow Capture Node
```python
from eric_metadata.utils.workflow_parser import WorkflowParser
from eric_metadata.utils.workflow_extractor import WorkflowExtractor
def capture_workflow(self, image_path, workflow_data=None):
"""Capture workflow information in metadata"""
# If no workflow data provided, try to extract from current context
if not workflow_data:
extractor = WorkflowExtractor()
workflow_data = extractor.extract_current_workflow()
# Parse workflow to extract key information
if workflow_data:
parser = WorkflowParser(workflow_data)
workflow_summary = parser.get_summary()
# Create metadata structure
metadata = {
'ai_info': {
'workflow_info': {
'node_count': workflow_summary.get('node_count', 0),
'execution_time': workflow_summary.get('execution_time', 0),
'creation_app': 'ComfyUI',
'timestamp': datetime.now().isoformat()
}
}
}
# Store full workflow data
if workflow_data:
# Store in database only to avoid size issues in embedded/XMP
db_metadata = {
'ai_info': {
'workflow': workflow_data
}
}
self.metadata_service.write_metadata(
image_path, db_metadata, targets=['database']
)
# Write summary to all targets
self.metadata_service.write_metadata(
image_path, metadata, targets=['embedded', 'xmp']
)
return workflow_summary
return None
```
## 11. Working with Namespaces
When extending the metadata system with custom fields or integrating with specific applications, you may need to register custom namespaces:
```python
from eric_metadata.utils.namespace import NamespaceManager
def register_custom_namespaces(self):
"""Register custom namespaces for specialized metadata"""
# Register a custom namespace for your application
NamespaceManager.register_namespace(
'myapp',
'http://example.org/myapp/1.0/',
register_with_pyexiv2=True
)
# Register an industry standard namespace
NamespaceManager.register_namespace(
'plus',
'http://ns.useplus.org/ldf/xmp/1.0/',
register_with_pyexiv2=True
)
# Get all registered namespaces
all_namespaces = NamespaceManager.get_all_namespaces()
print(f"Available namespaces: {all_namespaces}")
```
## 12. Debugging
Enable debug mode for detailed logging:
```python
# Enable debug logging in the service constructor
metadata_service = MetadataService(debug=True)
# Or enable it later
metadata_service.set_debug(True)
# Configure logging level for specific handlers
metadata_service.configure_handler('embedded', debug=True)
```
Common debug patterns:
- Check for XMP namespace registration issues with `NamespaceManager.print_registered_namespaces()`
- Verify file permissions when writing fails
- For XMP sidecar issues, ensure resource identifiers are set correctly
This guide is a preview and will be updated based on testing results and feedback. If you encounter any issues or have suggestions, please share them to help improve the system.