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