Skip to content

c-clark/aces-amf

 
 

ACES Metadata File (AMF)

License AMF Schema and Example Validation GitHub release (with filter) CLA assistant

The ACES Metadata File (AMF) is an XML sidecar format for exchanging the metadata required to reconstruct ACES viewing pipelines. It specifies the transforms needed to configure an ACES pipeline for a set of related image files.

This directory includes:

For details on the AMF format and its use cases, see the ACES Documentation.

Python Packages

This repository includes a suite of Python packages for working with AMF files programmatically. The packages are organized as a uv workspace monorepo under packages/.

Getting Started

# Install the high-level CLI and utilities (pulls in all dependencies)
pip install aces-amf-utils

# Or install individual packages
pip install aces-amf-lib        # core library only
pip install aces-transforms     # transform registry only

Package Overview

Package Description
aces-amf-utils CLI, builder, and utilities for creating, modifying, and analyzing AMF files
aces-amf-lib Reference library for reading, writing, and validating AMF files
aces-transforms ACES transform ID registry with offline lookup and version mapping
aces-common Shared protocols and types

Dependency Graph

aces-common          (shared protocols, zero dependencies)
  |
  +-- aces-transforms   (transform ID registry)
  |
  +-- aces-amf-lib      (AMF I/O, schema bindings, validation)
        |
        +-- aces-amf-utils  (CLI, builder, high-level API)

Getting Started

Load & Inspect an AMF

from aces.amf_utils import ACESAMF

amf = ACESAMF.from_file("shot_001.amf", validate=False)
print(amf.description)
print(amf.aces_version)  # (2, 0, 0)

if amf.input_transform:
    print(amf.input_transform.transform_id)
for idx, look in amf.iter_looks():
    print(f"  Look {idx}: {look.description}")
if amf.output_transform:
    print(amf.output_transform.transform_id)

Build a Complete AMF

Demonstrates all .with_* methods, including look transforms before and after a working location delimiter.

from aces.amf_lib import amf
from aces.amf_utils import ACESAMF
from aces.amf_utils.factories import cdl_look_transform

# CDL grade with ACEScct working space
primary_grade = cdl_look_transform(
    slope=(1.1, 1.0, 0.9),
    offset=(0.01, 0.0, -0.01),
    saturation=0.95,
)
primary_grade.description = "Primary Grade"
primary_grade.applied = False
primary_grade.cdl_working_space = amf.CdlWorkingSpaceType(
    from_cdl_working_space=amf.WorkingSpaceTransformType(
        transform_id="urn:ampas:aces:transformId:v2.0:CSC.Academy.ACEScct_to_ACES.a2.v1",
    ),
    to_cdl_working_space=amf.WorkingSpaceTransformType(
        transform_id="urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1",
    ),
)

# A file-based look (e.g. a show LUT)
show_look = amf.LookTransformType(
    file="show_lut.clf",
    description="Show LUT",
    applied=False,
)

amf = (
    ACESAMF.new(aces_version=(2, 0, 0))
    # Metadata
    .with_description("My Show - Ep 1")
    .with_pipeline_description("Camera to Rec.709 Display")
    .with_author(amf.AuthorType(name="Jane Doe", email_address="jane@example.com"))
    .with_clip_id(amf.ClipIdType(clip_name="A001C003", file="A001C003.ari"))
    # Input (camera to ACES)
    .with_input_transform(amf.InputTransformType(
        transform_id="urn:ampas:aces:transformId:v2.0:CSC.Arri.LogC4_to_ACES.a2.v1",
        description="ARRI LogC4 to ACES",
        applied=False,
    ))
    # Looks BEFORE working location (applied in ACES linear)
    .with_look_transform(primary_grade)
    # Working location delimiter
    .with_working_location()
    # Looks AFTER working location (applied in working color space)
    .with_look_transform(show_look)
    # Output (ACES to display)
    .with_output_transform(amf.OutputTransformType(
        transform_id="urn:ampas:aces:transformId:v2.0:Output.Academy.Rec709-D65_100nit_in_Rec709-D65_BT1886.a2.v1",
        description="Rec.709 100 nits",
        applied=False,
    ))
)

amf.write("output.amf", validate=False)

Load, Modify, and Save

from aces.amf_lib import amf
from aces.amf_utils import ACESAMF

amf = ACESAMF.from_file("shot_001.amf", validate=False)
amf.output_transform = amf.OutputTransformType(
    transform_id="urn:ampas:aces:transformId:v2.0:Output.Academy.P3-D65_1000nit_in_P3-D65_ST2084.a2.v1",
    description="P3 HDR 1000 nits",
    applied=False,
)
amf.write("shot_001_hdr.amf", validate=False)

Manage Look Transforms

Insert, remove, and reorder looks in an existing AMF.

from aces.amf_utils import ACESAMF
from aces.amf_utils.factories import cdl_look_transform

amf = ACESAMF.from_file("shot_001.amf", validate=False)

# List existing looks
for idx, look in amf.iter_looks():
    print(f"  [{idx}] {look.description}")

# Add a new look at position 0
new_look = cdl_look_transform(slope=(1.2, 1.0, 0.8))
new_look.description = "Show LUT"
new_look.applied = False
amf.insert_look(0, new_look)

# Remove a look by index
amf.remove_look(2)

# Reorder: move look 0 to position 1
amf.move_look(0, 1)

amf.write("reordered.amf", validate=False)

Validate an AMF

from aces.amf_lib import validate_schema, validate_semantic, validate_all
from aces.transforms import ACESTransformRegistry

registry = ACESTransformRegistry()

# Schema validation only (XSD)
messages = validate_schema("shot_001.amf")

# Semantic validation (includes transform ID checks)
messages = validate_semantic("shot_001.amf", transform_registry=registry)

# Both at once
messages = validate_all("shot_001.amf", transform_registry=registry)

for m in messages:
    print(f"[{m.level.value}] {m.message}")

Query the Transform Registry

Look up transforms, find equivalents, and check validity.

from aces.transforms import ACESTransformRegistry

registry = ACESTransformRegistry()

# List available ACES versions
print(registry.list_versions())  # ['v2.0.0+2025.04.04', 'v1.3.1', ...]

# Find transforms by category
for t in registry.list_transforms(category="CSC", version="v2.0")[:5]:
    print(t["transform_id"], "-", t["user_name"])

# Validate a transform ID
registry.is_valid_transform_id(
    "urn:ampas:aces:transformId:v2.0:CSC.Arri.LogC4_to_ACES.a2.v1"
)  # True

# Resolve a legacy URN to its current equivalent
registry.get_equivalent_id(
    "urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3"
)
# -> 'urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1'

Compare Two AMFs

from aces.amf_utils import diff_amf

result = diff_amf("shot_001_v1.amf", "shot_001_v2.amf")
if result.has_differences:
    print(result.summary())
    for d in result.differences:
        print(f"  {d.field}: {d.old_value} -> {d.new_value}")

Parse Transform URNs

from aces.common import TransformURN

urn = TransformURN.parse(
    "urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3"
)
print(urn.spec_version)    # v1.5
print(urn.transform_type)  # ACEScsc
print(urn.namespace)       # Academy
print(urn.name)            # ACES_to_ACEScct
print(urn.version_suffix)  # a1.0.3
print(urn.is_v1)           # True

CLI Quick Reference

# Validate AMF files
amf validate shot_001.amf shot_002.amf

# Display AMF info
amf info shot_001.amf

# Create a new AMF
amf create output.amf -d "My Shot" \
  --idt "urn:ampas:aces:transformId:v2.0:CSC.Arri.LogC4_to_ACES.a2.v1" \
  --odt "urn:ampas:aces:transformId:v2.0:Output.Academy.Rec709-D65_100nit_in_Rec709-D65_BT1886.a2.v1"

# Add a CDL grade
amf add-cdl shot_001.amf --slope 1.1 1.0 0.9 --saturation 0.95 -o graded.amf

# Compare two AMFs
amf diff shot_001_v1.amf shot_001_v2.amf

# Convert v1 to v2
amf convert legacy.amf -o modern.amf

# Query transforms
amf transforms list -c CSC -n 10
amf transforms info "urn:ampas:aces:transformId:v2.0:CSC.Arri.LogC4_to_ACES.a2.v1"

# Fix version-mismatched URNs
amf resolve-urns mixed.amf --auto -o fixed.amf

Lower-level access: For direct Pydantic model access, AMFBuilder(...).build() returns a raw AcesMetadataFile, and load_amf() / save_amf() from aces.amf_lib provide I/O without the wrapper. See the aces-amf-lib README for details.

Development Setup

# Clone and set up the workspace
git clone https://github.com/ampas/aces-amf.git
cd aces-amf
uv sync

# Run all tests
uv run pytest

Requires Python >= 3.10.

Contributing

ACES depends on community participation. Developers, manufacturers, and end users are encouraged to contribute code, bug fixes, documentation, and other technical artifacts.

All contributors must have a signed Contributor License Agreement (CLA) on file to ensure that the project can freely use your contributions.

See CONTRIBUTING.md for more details.

Governance

This repository is a submodule of the ACES project, which is governed by the Academy Software Foundation.

For details about how the project operates, refer to the GOVERNANCE.md file found in in the top-level ACES repository.

Reporting Issues

To report a problem with AMF, please open an issue.

If the issue is senstive in nature or a security related issue, please do not report in the issue tracker. Instead refer to SECURITY.md for more information about the project security policy.

License

The ACES Project is licensed under the Apache 2.0 license.

About

ACES Metadata File (AMF)

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 98.7%
  • Shell 1.3%