343 lines
12 KiB
Python
343 lines
12 KiB
Python
"""
|
|
metadata.py
|
|
Description: Metadata model for handling image metadata, including basic info, analysis data, and AI generation data.
|
|
Author: Eric Hiss (GitHub: EricRollei)
|
|
Contact: [eric@historic.camera, eric@rollei.us]
|
|
Version: 1.0.0
|
|
Date: [March 2025]
|
|
License: Dual License (Non-Commercial and Commercial Use)
|
|
Copyright (c) 2025 Eric Hiss. All rights reserved.
|
|
|
|
Dual License:
|
|
1. Non-Commercial Use: This software is licensed under the terms of the
|
|
Creative Commons Attribution-NonCommercial 4.0 International License.
|
|
To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
|
|
|
|
2. Commercial Use: For commercial use, a separate license is required.
|
|
Please contact Eric Hiss at [eric@historic.camera, eric@rollei.us] for licensing options.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
|
|
Dependencies:
|
|
This code depends on several third-party libraries, each with its own license:
|
|
|
|
"""
|
|
# Metadata_system/src/eric_metadata/models/metadata.py
|
|
from typing import Dict, Any, List, Optional, Union, Set
|
|
import datetime
|
|
import json
|
|
|
|
class MetadataModel:
|
|
"""Base model for metadata structures"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the metadata model with empty structure"""
|
|
self.data = {
|
|
'basic': {},
|
|
'analysis': {},
|
|
'ai_info': {}
|
|
}
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert model to dictionary"""
|
|
return self.data
|
|
|
|
def from_dict(self, data: Dict[str, Any]) -> 'MetadataModel':
|
|
"""Load model from dictionary"""
|
|
self.data = data
|
|
return self
|
|
|
|
def add_basic_metadata(self, title: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
keywords: Optional[Union[List[str], Set[str], str]] = None,
|
|
rating: Optional[int] = None) -> 'MetadataModel':
|
|
"""
|
|
Add basic metadata
|
|
|
|
Args:
|
|
title: Image title
|
|
description: Image description
|
|
keywords: Image keywords
|
|
rating: Image rating (0-5)
|
|
|
|
Returns:
|
|
self for chaining
|
|
"""
|
|
if title is not None:
|
|
self.data['basic']['title'] = title
|
|
|
|
if description is not None:
|
|
self.data['basic']['description'] = description
|
|
|
|
if keywords is not None:
|
|
# Handle different keyword formats
|
|
if isinstance(keywords, str):
|
|
# Split by commas
|
|
kw_list = [k.strip() for k in keywords.split(',')]
|
|
self.data['basic']['keywords'] = kw_list
|
|
elif isinstance(keywords, (list, set)):
|
|
self.data['basic']['keywords'] = list(keywords)
|
|
|
|
if rating is not None:
|
|
# Validate rating range
|
|
rating = max(0, min(5, rating))
|
|
self.data['basic']['rating'] = rating
|
|
|
|
return self
|
|
|
|
def add_analysis_data(self, analysis_type: str, data: Dict[str, Any]) -> 'MetadataModel':
|
|
"""
|
|
Add analysis data
|
|
|
|
Args:
|
|
analysis_type: Type of analysis (technical, aesthetic, etc.)
|
|
data: Analysis data
|
|
|
|
Returns:
|
|
self for chaining
|
|
"""
|
|
if analysis_type not in self.data['analysis']:
|
|
self.data['analysis'][analysis_type] = {}
|
|
|
|
# Add timestamp if not present
|
|
if 'timestamp' not in data:
|
|
data['timestamp'] = datetime.datetime.now().isoformat()
|
|
|
|
# Merge with existing data
|
|
self.data['analysis'][analysis_type].update(data)
|
|
|
|
return self
|
|
|
|
def add_ai_generation_data(self, model: Optional[str] = None,
|
|
prompt: Optional[str] = None,
|
|
negative_prompt: Optional[str] = None,
|
|
seed: Optional[int] = None,
|
|
steps: Optional[int] = None,
|
|
cfg_scale: Optional[float] = None,
|
|
sampler: Optional[str] = None,
|
|
scheduler: Optional[str] = None,
|
|
loras: Optional[List[Dict[str, Any]]] = None) -> 'MetadataModel':
|
|
"""
|
|
Add AI generation data with improved structure
|
|
|
|
Args:
|
|
model: Model name
|
|
prompt: Positive prompt
|
|
negative_prompt: Negative prompt
|
|
seed: Generation seed
|
|
steps: Number of steps
|
|
cfg_scale: CFG scale
|
|
sampler: Sampler name
|
|
scheduler: Scheduler name
|
|
loras: List of LoRA configurations
|
|
|
|
Returns:
|
|
self for chaining
|
|
"""
|
|
if 'ai_info' not in self.data:
|
|
self.data['ai_info'] = {}
|
|
|
|
if 'generation' not in self.data['ai_info']:
|
|
self.data['ai_info']['generation'] = {}
|
|
|
|
generation = self.data['ai_info']['generation']
|
|
|
|
# Create structured hierarchical data
|
|
|
|
# Model information
|
|
if model is not None:
|
|
if 'base_model' not in generation:
|
|
generation['base_model'] = {}
|
|
generation['base_model']['unet'] = model
|
|
|
|
# Sampling parameters
|
|
if any(param is not None for param in [seed, steps, cfg_scale, sampler, scheduler]):
|
|
if 'sampling' not in generation:
|
|
generation['sampling'] = {}
|
|
|
|
if seed is not None:
|
|
generation['sampling']['seed'] = seed
|
|
|
|
if steps is not None:
|
|
generation['sampling']['steps'] = steps
|
|
|
|
if cfg_scale is not None:
|
|
generation['sampling']['cfg_scale'] = cfg_scale
|
|
|
|
if sampler is not None:
|
|
generation['sampling']['sampler'] = sampler
|
|
|
|
if scheduler is not None:
|
|
generation['sampling']['scheduler'] = scheduler
|
|
|
|
# Prompts
|
|
if prompt is not None:
|
|
generation['prompt'] = prompt
|
|
|
|
if negative_prompt is not None:
|
|
generation['negative_prompt'] = negative_prompt
|
|
|
|
# LoRAs
|
|
if loras is not None:
|
|
if 'modules' not in generation:
|
|
generation['modules'] = {}
|
|
|
|
generation['modules']['loras'] = loras
|
|
|
|
# Add timestamp if not present
|
|
if 'timestamp' not in generation:
|
|
generation['timestamp'] = datetime.datetime.now().isoformat()
|
|
|
|
return self
|
|
def add_face_region(self, x: float, y: float, w: float, h: float,
|
|
name: Optional[str] = None,
|
|
face_data: Optional[Dict[str, Any]] = None) -> 'MetadataModel':
|
|
"""
|
|
Add face region
|
|
|
|
Args:
|
|
x: X coordinate (normalized 0-1)
|
|
y: Y coordinate (normalized 0-1)
|
|
w: Width (normalized 0-1)
|
|
h: Height (normalized 0-1)
|
|
name: Face name
|
|
face_data: Face analysis data
|
|
|
|
Returns:
|
|
self for chaining
|
|
"""
|
|
if 'regions' not in self.data:
|
|
self.data['regions'] = {
|
|
'summary': {
|
|
'face_count': 0,
|
|
'detector_type': None
|
|
},
|
|
'faces': []
|
|
}
|
|
|
|
# Create face region
|
|
face = {
|
|
'type': 'Face',
|
|
'name': name or f"Face {len(self.data['regions']['faces']) + 1}",
|
|
'area': {
|
|
'x': max(0.0, min(1.0, x)),
|
|
'y': max(0.0, min(1.0, y)),
|
|
'w': max(0.0, min(1.0, w)),
|
|
'h': max(0.0, min(1.0, h))
|
|
}
|
|
}
|
|
|
|
# Add face analysis data if provided
|
|
if face_data:
|
|
face['extensions'] = {
|
|
'eiqa': {
|
|
'face_analysis': {}
|
|
}
|
|
}
|
|
|
|
face_analysis = face['extensions']['eiqa']['face_analysis']
|
|
|
|
# Add basic fields
|
|
for field in ['age', 'dominant_gender', 'dominant_race', 'dominant_emotion']:
|
|
if field in face_data:
|
|
face_analysis[field] = face_data[field]
|
|
|
|
# Add detailed scores if present
|
|
if 'gender' in face_data:
|
|
face_analysis['gender'] = {
|
|
'scores': face_data['gender']
|
|
}
|
|
|
|
if 'race' in face_data:
|
|
face_analysis['race'] = {
|
|
'scores': face_data['race']
|
|
}
|
|
|
|
if 'emotion' in face_data:
|
|
face_analysis['emotion'] = {
|
|
'scores': face_data['emotion']
|
|
}
|
|
|
|
# Add face to regions
|
|
self.data['regions']['faces'].append(face)
|
|
|
|
# Update face count
|
|
self.data['regions']['summary']['face_count'] = len(self.data['regions']['faces'])
|
|
|
|
return self
|
|
|
|
def merge(self, other: Union[Dict[str, Any], 'MetadataModel']) -> 'MetadataModel':
|
|
"""
|
|
Merge with another metadata model or dictionary
|
|
|
|
Args:
|
|
other: Another metadata model or dictionary
|
|
|
|
Returns:
|
|
self for chaining
|
|
"""
|
|
# Convert to dict if needed
|
|
other_dict = other.to_dict() if isinstance(other, MetadataModel) else other
|
|
|
|
# Merge basic metadata
|
|
if 'basic' in other_dict:
|
|
for key, value in other_dict['basic'].items():
|
|
if key == 'keywords':
|
|
# Combine keywords
|
|
existing_keywords = set(self.data['basic'].get('keywords', []))
|
|
new_keywords = set(value if isinstance(value, list) else [value])
|
|
self.data['basic']['keywords'] = list(existing_keywords | new_keywords)
|
|
else:
|
|
# Replace other basic fields
|
|
self.data['basic'][key] = value
|
|
|
|
# Merge analysis data
|
|
if 'analysis' in other_dict:
|
|
for analysis_type, data in other_dict['analysis'].items():
|
|
self.add_analysis_data(analysis_type, data)
|
|
|
|
# Merge AI info
|
|
if 'ai_info' in other_dict:
|
|
if 'generation' in other_dict['ai_info']:
|
|
generation = other_dict['ai_info']['generation']
|
|
self.add_ai_generation_data(
|
|
model=generation.get('model'),
|
|
prompt=generation.get('prompt'),
|
|
negative_prompt=generation.get('negative_prompt'),
|
|
seed=generation.get('seed'),
|
|
steps=generation.get('steps'),
|
|
cfg_scale=generation.get('cfg_scale'),
|
|
sampler=generation.get('sampler'),
|
|
scheduler=generation.get('scheduler'),
|
|
loras=generation.get('loras')
|
|
)
|
|
|
|
# Merge regions
|
|
if 'regions' in other_dict:
|
|
# Add faces
|
|
for face in other_dict['regions'].get('faces', []):
|
|
area = face.get('area', {})
|
|
face_data = face.get('extensions', {}).get('eiqa', {}).get('face_analysis', {})
|
|
|
|
self.add_face_region(
|
|
x=area.get('x', 0),
|
|
y=area.get('y', 0),
|
|
w=area.get('w', 0),
|
|
h=area.get('h', 0),
|
|
name=face.get('name'),
|
|
face_data=face_data
|
|
)
|
|
|
|
return self
|
|
|
|
def to_json(self) -> str:
|
|
"""Convert model to JSON string"""
|
|
return json.dumps(self.data, indent=2)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_str: str) -> 'MetadataModel':
|
|
"""Create model from JSON string"""
|
|
data = json.loads(json_str)
|
|
return cls().from_dict(data) |