234 lines
8.8 KiB
Python
234 lines
8.8 KiB
Python
"""
|
|
xmp_helpers.py
|
|
Description: Utilities for creating and manipulating XMP metadata for AI-generated images.
|
|
This module provides functions to register namespaces, create XMP packets from metadata,
|
|
and extract metadata from XMP packets.
|
|
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:
|
|
|
|
XMP Metadata Helpers
|
|
|
|
Utilities for working with XMP metadata specifically for AI image generation.
|
|
Provides namespace registration and standardized structuring for compatibility
|
|
with image cataloging software.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
def register_ai_namespaces():
|
|
"""
|
|
Register AI-specific namespaces for XMP
|
|
|
|
This ensures that when writing XMP, the proper namespace prefixes are used
|
|
"""
|
|
# Standard namespaces
|
|
ET.register_namespace('x', 'adobe:ns:meta/')
|
|
ET.register_namespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
|
|
ET.register_namespace('xmp', 'http://ns.adobe.com/xap/1.0/')
|
|
ET.register_namespace('dc', 'http://purl.org/dc/elements/1.1/')
|
|
ET.register_namespace('photoshop', 'http://ns.adobe.com/photoshop/1.0/')
|
|
ET.register_namespace('xmpRights', 'http://ns.adobe.com/xap/1.0/rights/')
|
|
|
|
# Custom AI namespaces
|
|
ET.register_namespace('ai', 'http://ericproject.org/schemas/ai/1.0/')
|
|
ET.register_namespace('eiqa', 'http://ericproject.org/schemas/eiqa/1.0/')
|
|
ET.register_namespace('sd', 'http://ns.adobe.com/diffusion/1.0/')
|
|
|
|
def create_xmp_from_metadata(metadata: Dict[str, Any]) -> str:
|
|
"""
|
|
Create XMP XML string from metadata dictionary with improved AI structure
|
|
|
|
Args:
|
|
metadata: Metadata dictionary with AI generation info
|
|
|
|
Returns:
|
|
str: XMP packet as XML string
|
|
"""
|
|
# Register custom namespaces
|
|
register_ai_namespaces()
|
|
|
|
# Create base XMP structure
|
|
xmpmeta = ET.Element("{adobe:ns:meta/}xmpmeta")
|
|
|
|
# Add namespaces
|
|
namespaces = {
|
|
'x': 'adobe:ns:meta/',
|
|
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
'xmp': 'http://ns.adobe.com/xap/1.0/',
|
|
'dc': 'http://purl.org/dc/elements/1.1/',
|
|
'photoshop': 'http://ns.adobe.com/photoshop/1.0/',
|
|
'xmpRights': 'http://ns.adobe.com/xap/1.0/rights/',
|
|
'ai': 'http://ericproject.org/schemas/ai/1.0/',
|
|
'eiqa': 'http://ericproject.org/schemas/eiqa/1.0/',
|
|
'sd': 'http://ns.adobe.com/diffusion/1.0/'
|
|
}
|
|
|
|
for prefix, uri in namespaces.items():
|
|
xmpmeta.set(f"xmlns:{prefix}", uri)
|
|
|
|
# Create RDF element
|
|
rdf = ET.SubElement(xmpmeta, f"{{{namespaces['rdf']}}}RDF")
|
|
|
|
# Create Description element
|
|
desc = ET.SubElement(rdf, f"{{{namespaces['rdf']}}}Description")
|
|
desc.set(f"{{{namespaces['rdf']}}}about", "")
|
|
|
|
# Add basic metadata
|
|
if 'basic' in metadata:
|
|
basic = metadata['basic']
|
|
|
|
# Handle title, description, etc.
|
|
if 'title' in basic:
|
|
dc_title = ET.SubElement(desc, f"{{{namespaces['dc']}}}title")
|
|
_add_language_alt(dc_title, basic['title'], namespaces)
|
|
|
|
if 'description' in basic:
|
|
dc_desc = ET.SubElement(desc, f"{{{namespaces['dc']}}}description")
|
|
_add_language_alt(dc_desc, basic['description'], namespaces)
|
|
|
|
if 'creator' in basic:
|
|
dc_creator = ET.SubElement(desc, f"{{{namespaces['dc']}}}creator")
|
|
_add_seq(dc_creator, [basic['creator']], namespaces)
|
|
|
|
if 'keywords' in basic:
|
|
keywords = basic['keywords']
|
|
if isinstance(keywords, str):
|
|
keywords = [k.strip() for k in keywords.split(',') if k.strip()]
|
|
dc_subject = ET.SubElement(desc, f"{{{namespaces['dc']}}}subject")
|
|
_add_bag(dc_subject, keywords, namespaces)
|
|
|
|
# Add AI generation info - enhanced structure
|
|
if 'ai_info' in metadata:
|
|
ai_info = metadata['ai_info']
|
|
|
|
# Add generation information
|
|
if 'generation' in ai_info:
|
|
gen = ai_info['generation']
|
|
|
|
# Simplified flat structure for better compatibility
|
|
for key, value in gen.items():
|
|
if key == 'loras':
|
|
# Handle loras specially
|
|
continue
|
|
if value is not None:
|
|
elem = ET.SubElement(desc, f"{{{namespaces['ai']}}}{key}")
|
|
elem.text = str(value)
|
|
|
|
# Add LoRAs
|
|
if 'loras' in gen and gen['loras']:
|
|
loras_elem = ET.SubElement(desc, f"{{{namespaces['ai']}}}loras")
|
|
_add_lora_seq(loras_elem, gen['loras'], namespaces)
|
|
|
|
# Add workflow metadata in a structured way
|
|
if 'workflow_info' in ai_info:
|
|
workflow_info = ai_info['workflow_info']
|
|
for key, value in workflow_info.items():
|
|
if value is not None:
|
|
elem = ET.SubElement(desc, f"{{{namespaces['ai']}}}{key}")
|
|
elem.text = str(value)
|
|
|
|
# Convert to string with proper formatting
|
|
_indent_xml(xmpmeta)
|
|
return ET.tostring(xmpmeta, encoding='unicode')
|
|
|
|
def _add_language_alt(parent, value, namespaces):
|
|
"""Add an RDF language alternative structure"""
|
|
alt = ET.SubElement(parent, f"{{{namespaces['rdf']}}}Alt")
|
|
li = ET.SubElement(alt, f"{{{namespaces['rdf']}}}li")
|
|
li.set('xml:lang', 'x-default')
|
|
li.text = str(value)
|
|
|
|
def _add_seq(parent, values, namespaces):
|
|
"""Add an RDF sequence structure"""
|
|
seq = ET.SubElement(parent, f"{{{namespaces['rdf']}}}Seq")
|
|
for value in values:
|
|
li = ET.SubElement(seq, f"{{{namespaces['rdf']}}}li")
|
|
li.text = str(value)
|
|
|
|
def _add_bag(parent, values, namespaces):
|
|
"""Add an RDF bag structure"""
|
|
bag = ET.SubElement(parent, f"{{{namespaces['rdf']}}}Bag")
|
|
for value in values:
|
|
li = ET.SubElement(bag, f"{{{namespaces['rdf']}}}li")
|
|
li.text = str(value)
|
|
|
|
def _add_lora_seq(parent, loras, namespaces):
|
|
"""
|
|
Add a sequence of LoRA entries with their properties - improved structure
|
|
|
|
Args:
|
|
parent: Parent XML element to add the sequence to
|
|
loras: List of LoRA entries (dicts or strings)
|
|
namespaces: Dictionary of namespace URI mappings
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
try:
|
|
seq = ET.SubElement(parent, f"{{{namespaces['rdf']}}}Seq")
|
|
|
|
for lora in loras:
|
|
li = ET.SubElement(seq, f"{{{namespaces['rdf']}}}li")
|
|
|
|
if isinstance(lora, dict):
|
|
# Complex LoRA entry with properties
|
|
lora_desc = ET.SubElement(li, f"{{{namespaces['rdf']}}}Description")
|
|
|
|
# Required properties
|
|
if 'name' in lora:
|
|
name_elem = ET.SubElement(lora_desc, f"{{{namespaces['ai']}}}name")
|
|
name_elem.text = str(lora['name'])
|
|
|
|
# Strength parameters
|
|
for strength_key in ['strength', 'model_strength', 'clip_strength']:
|
|
if strength_key in lora:
|
|
strength_elem = ET.SubElement(lora_desc, f"{{{namespaces['ai']}}}{strength_key}")
|
|
strength_elem.text = str(lora[strength_key])
|
|
else:
|
|
# Simple LoRA name string
|
|
li.text = str(lora)
|
|
except Exception as e:
|
|
print(f"Error creating LoRA sequence: {str(e)}")
|
|
|
|
def _indent_xml(elem, level=0):
|
|
"""
|
|
Add proper indentation to XML for readability
|
|
|
|
Args:
|
|
elem: XML element
|
|
level: Indentation level
|
|
"""
|
|
i = "\n" + level * " "
|
|
if len(elem):
|
|
if not elem.text or not elem.text.strip():
|
|
elem.text = i + " "
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
for elem in elem:
|
|
_indent_xml(elem, level + 1)
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
else:
|
|
if level and (not elem.tail or not elem.tail.strip()):
|
|
elem.tail = i
|