Files
EricRollei-AAA_Metadata_System/eric_metadata/models/metadata.py
T

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)