Introduction

Overview

The goal of this format is to facilitate interchange of annotations and notes between different review systems or provide a very simple offline review format for a simple review system.

The ORIAnnotations module provides a set of helper classes to manage media and annotations, primarily using OpenTimelineIO (OTIO) as the interchange format. This allows for easy integration with various production tracking systems without requiring specialized tools.

Requirements

The main requirement is on Opentimelineio.

pip install opentimelineio

However, to build the documentation, you also need:

Example Usage

  1import sys, os
  2
  3project_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
  4
  5manifest_path = os.environ.get("OTIO_PLUGIN_MANIFEST_PATH", "")
  6if manifest_path:
  7    manifest_path += os.pathsep
  8os.environ["OTIO_PLUGIN_MANIFEST_PATH"] = manifest_path + os.path.join(
  9    project_root, "otio_event_plugin", "plugin_manifest.json"
 10)
 11sys.path.append(os.path.join(project_root, "python"))
 12
 13
 14
 15import opentimelineio as otio
 16import ORIAnnotations
 17import os
 18import dataclasses
 19
 20
 21reviewdata = [
 22    {'media': "/Users/sam/git/Annotations/testmedia/chimera_cars_srgb-test_mov-prores_ks.mov",
 23     'startframe': 0,
 24     'framerate': 25,
 25     'duration': 200,
 26     'reviewframes': [{'note': "Hello", 
 27                       'frame': 1, 
 28                       'image': '/Users/sam/git/Annotations/test_export/chimera_cars_srgb-test_mov-prores_ks.00001.png'
 29                       },
 30                      {'note': "Hello", 
 31                       'frame': 15, 
 32                       'image': '/Users/sam/git/Annotations/test_export/chimera_cars_srgb-test_mov-prores_ks.00015.png',
 33                       "annotation_commands": """
 34                           {"OTIO_SCHEMA": "PaintStart.1", "brush": "circle","friendly_name": "defaultSequence_p_sourceGroup000000.pen:13:1:sam","rgba": [1,1,1,1],"source_index": 0,"timestamp": "2025-07-06T23:21:48.664116","type": "color","uuid": "6500d2f3-e758-4cd6-9bf2-83c7d712786d","visible": true}
 35                           {"OTIO_SCHEMA": "PaintPoint.1", "point": [{"OTIO_SCHEMA": "PaintVertex.1", "size": 0.00995635986328125, "x": -0.47882136702537537,"y": 0.046961307525634766},{"OTIO_SCHEMA": "PaintVertex.1","size": 0.00995635986328125,"x": -0.480663001537323,"y": 0.046961307525634766}]}
 36"""
 37                    },
 38          ]
 39     },
 40     {'media': "/Users/sam/git/Annotations/testmedia/chimera_coaster_srgb-test_mov-dnxhd.mov",
 41        'startframe': 0,
 42        'framerate': 25,
 43        'duration': 200,
 44        'reviewframes': [{'note': "Hello", 'frame': 1, 'image': '/Users/sam/git/Annotations/test_export/chimera_coaster_srgb-test_mov-dnxhd.00001.png'},
 45                        {'note': "Hello", 'frame': 25, 'image': '/Users/sam/git/Annotations/test_export/chimera_coaster_srgb-test_mov-dnxhd.00025.png'},
 46        ]
 47      },
 48      {'media': "/Users/sam/git/Annotations/testmedia/chimera_fountains_srgb-test_mov-dnxhd.mov",
 49        'startframe': 0,
 50        'framerate': 25,
 51        'duration': 200,
 52        'reviewframes': [{'note': "Hello", 'frame': 28, 'image': '/Users/sam/git/Annotations/test_export/chimera_fountains_srgb-test_mov-dnxhd.00028.png'},
 53        ]
 54       }
 55]
 56
 57
 58medialist = []
 59reviewitems = []
 60
 61for reviewmedia in reviewdata:
 62    media = ORIAnnotations.Media(media_path=reviewmedia['media'], 
 63                                 name=os.path.basename(reviewmedia['media']), 
 64                                 frame_rate=reviewmedia['framerate'], 
 65                                 start_frame=reviewmedia['startframe'],
 66                                 duration=reviewmedia['duration']
 67                                 )
 68    medialist.append(media)
 69    ri = ORIAnnotations.ReviewItem(media=media)
 70    reviewitems.append(ri)
 71    frames = []
 72    for reviewframe in reviewmedia['reviewframes']:
 73        frame = ORIAnnotations.ReviewItemFrame(note=reviewframe['note'], annotation_image=reviewframe['image'], frame=reviewframe['frame'], review_item=ri)
 74        if 'annotation_commands' in reviewframe:
 75            # For testing we assume this is a jsonlines format string, so we are going to read it in and turn it into OTIO schema objects.
 76            lines = reviewframe['annotation_commands'].splitlines()
 77            events = []
 78            for line in lines:
 79                if len(line) == 0:
 80                    continue
 81                print("DECODING:", line)
 82                event = otio.adapters.read_from_string(line, adapter_name="otio_json")
 83                events.append(event)
 84            frame.annotation_commands = events
 85        frames.append(frame)
 86    ri.review_frames = frames
 87
 88review = ORIAnnotations.Review(title="Review", review_items=reviewitems)
 89print("MediaList:", medialist)
 90reviewgroup = ORIAnnotations.ReviewGroup(media=medialist, reviews=[review])
 91timeline = reviewgroup.export_otio_timeline()
 92print("About to export:")
 93outputfile = "tests/test_export_unittest.otio"
 94otio.adapters.write_to_file(timeline, outputfile)
 95print("Exported to:", outputfile)
 96
 97
 98# Now we read the file back in.
 99
100newtimeline = otio.adapters.read_from_file(outputfile)
101rg = ORIAnnotations.ReviewGroup()
102rg.read_otio_timeline(newtimeline)
103alt_test_output_file = "tests/test_export_unittest2.otio"
104otio.adapters.write_to_file(newtimeline, alt_test_output_file)
105print("Exported to:", alt_test_output_file)

You can run this from the command line, once the libraries are installed with:

python test/testannotations.py
See also: