OSL JSON Format¶
OSL JSON is the canonical annotation format used across OpenSportsLab tools. OpenSportsLib uses it for dataset manifests, ground-truth annotations, and prediction payloads returned by the high-level APIs.
An OSL JSON file is a single JSON object with project metadata, a shared label
schema, and a data array of samples. Each sample points to one or more input
files and can carry task-specific annotations.
The current OpenSportsLib implementation supports classification, localization, and VQA workflows. The format also reserves payloads for description and dense description tasks so datasets can stay compatible with the broader OpenSportsLab ecosystem.
Minimal Structure¶
The smallest useful file is a JSON object with a data list. For training and
evaluation, include a root labels schema and task-specific sample payloads.
{
"version": "2.0",
"date": "2026-05-19",
"dataset_name": "soccer-demo",
"description": "Example OSL dataset.",
"modalities": ["video"],
"metadata": {
"sport": "soccer",
"split": "train"
},
"labels": {
"action": {
"type": "single_label",
"labels": ["pass", "shot", "foul"]
}
},
"data": []
}
Top-Level Fields¶
| Field | Type | Required | Notes |
|---|---|---|---|
version |
string | Recommended | Current canonical version is "2.0". |
date |
string | Recommended | ISO date such as "2026-05-19". |
dataset_name |
string | Recommended | Human-readable dataset or split name. |
description |
string | Optional | Free-text dataset description. |
modalities |
array[string] | Recommended | Input types present in data[].inputs[], such as ["video"]. |
metadata |
object | Optional | Dataset-level custom metadata. |
labels |
object | Required for supervised tasks | Shared label schema by annotation head. |
data |
array[object] | Required | Sample list. Must be a list. |
Unknown top-level keys are preserved by conversion tools where possible. Keep
custom dataset metadata under metadata unless another key is part of a
documented workflow.
Label Schema¶
The root labels object defines annotation heads. Each head name is a key, and
each definition should include:
| Field | Type | Notes |
|---|---|---|
type |
string | Use single_label for one class per sample/event, or multi_label for several labels. |
labels |
array[string] | Allowed class names for this head. |
{
"labels": {
"action": {
"type": "single_label",
"labels": ["pass", "shot", "foul"]
},
"attributes": {
"type": "multi_label",
"labels": ["left_foot", "header", "set_piece"]
}
}
}
OpenSportsLib classification currently reads the action head by default:
data[].labels.action.label. Localization event heads should also point to the
same root schema, for example data[].events[].head == "action".
Sample Objects¶
Each entry in data is one sample.
| Field | Type | Notes |
|---|---|---|
id |
string | Stable sample ID. Required for reliable evaluation and prediction matching. |
inputs |
array[object] | Media, feature, or tracking files for the sample. |
metadata |
object | Optional sample-level metadata such as match, game, clip, or timing fields. |
labels |
object | Classification annotations keyed by label head. |
events |
array[object] | Timestamped localization events. |
captions |
array[object] | Clip-level description captions. |
dense_captions |
array[object] | Timestamped dense descriptions. |
answers |
array[object] | Grouped question/answer annotations. |
Unknown sample keys are preserved by conversion tools where possible.
Input Objects¶
Every sample should include inputs, even if it has only one input file.
Supported input types used by current OpenSportsLib workflows:
| Type | Typical path | Notes |
|---|---|---|
video |
clips/clip_0001.mp4 |
Raw video clip or full game video. |
frames_npy |
frames/clip_0001.npy |
NumPy frame array. The legacy alias frame_npy is normalized by annotation tooling. |
tracking_parquet |
tracking/clip_0001.parquet |
Parquet tracking data. |
fps is recommended for video and frame-array inputs. Tracking inputs can also
include fps as a fallback when timestamps are not available.
Relative Path Resolution¶
OpenSportsLib stores input paths as relative paths inside JSON whenever possible.
- Classification and localization training/inference resolve
inputs[].pathfrom the configured split media root, usuallyDATA.common.splits.<split>.source_path. - Feature-based localization resolves
inputs[0].pathfrom the configured feature directory. - Conversion tools resolve
inputs[].pathfrom themedia_rootargument passed toconvert_json_to_parquet(...)or the CLI wrapper. - Hugging Face upload/download tools treat
inputs[].pathas repository paths relative to the JSON file or selected split.
Example directory layout:
dataset/
├── train/
│ ├── clips/clip_0001.mp4
│ └── annotations_train.json
├── valid/
│ ├── clips/clip_0101.mp4
│ └── annotations_valid.json
└── test/
├── clips/clip_0201.mp4
└── annotations_test.json
For the train split, set DATA.common.splits.train.source_path to
dataset/train and store the sample path as clips/clip_0001.mp4.
Multi-Input And Multi-View Samples¶
Use multiple inputs entries when a sample has more than one synchronized view
or modality.
{
"id": "play_0001",
"inputs": [
{
"type": "video",
"path": "wide/play_0001.mp4",
"fps": 25.0
},
{
"type": "video",
"path": "close/play_0001.mp4",
"fps": 25.0
},
{
"type": "tracking_parquet",
"path": "tracking/play_0001.parquet"
}
]
}
Classification multi-view loading groups samples by id when the config uses
DATA.view_type: multi. The current grouping helper also supports IDs with a
_view suffix, such as play_0001_view1 and play_0001_view2.
Classification Payload¶
Classification labels live under data[].labels. The key under labels should
match a root label head.
{
"id": "clip_0001",
"inputs": [
{
"type": "video",
"path": "clips/clip_0001.mp4",
"fps": 25.0
}
],
"labels": {
"action": {
"label": "shot"
},
"attributes": {
"labels": ["left_foot", "set_piece"]
}
}
}
For classification training, OpenSportsLib expects labels.action.label by
default. Samples without labels can be used for test/inference splits, but
training and validation need labels.
Smart predictions may include a confidence score. The annotation-tool convention
is confidence_score; current OpenSportsLib prediction exporters use
confidence.
Localization Payload¶
Localization annotations live under data[].events. Each event is a point
timestamp in milliseconds.
{
"events": [
{
"head": "action",
"label": "pass",
"position_ms": 1240
},
{
"head": "action",
"label": "shot",
"position_ms": 4320,
"gameTime": "1 - 00:04",
"confidence_score": 0.84
}
]
}
OpenSportsLib localization prefers position_ms when present. If
position_ms is missing, feature-based JSON loaders fall back to gameTime.
For predictions and evaluation, current OpenSportsLib spotting outputs use
confidence.
Description, Dense Description, And Q/A Payloads¶
These payloads are part of the OSL JSON ecosystem. They are useful for datasets that need to round-trip through OpenSportsLab annotation tools. Q/A payloads are used by the OpenSportsLib VQA workflow.
Clip-level captions:
Timestamped dense captions:
{
"dense_captions": [
{
"position_ms": 1100,
"lang": "en",
"text": "The midfielder plays a forward pass."
},
{
"position_ms": 3650,
"lang": "en",
"text": "The striker shoots from inside the area."
}
]
}
Grouped question/answer annotations:
{
"id": "clip_0001",
"inputs": [
{
"type": "video",
"path": "clips/clip_0001.mp4",
"fps": 25.0
}
],
"answers": [
{
"question": "What card would you give? Why?",
"answers": ["No card, because this is a fair challenge."]
}
]
}
Complete Classification Example¶
{
"version": "2.0",
"date": "2026-05-19",
"task": "action_classification",
"dataset_name": "soccer-classification-demo",
"description": "Clip-level action labels.",
"modalities": ["video"],
"metadata": {
"sport": "soccer",
"split": "train"
},
"labels": {
"action": {
"type": "single_label",
"labels": ["pass", "shot", "foul"]
},
"attributes": {
"type": "multi_label",
"labels": ["left_foot", "header", "set_piece"]
}
},
"data": [
{
"id": "clip_0001",
"inputs": [
{
"type": "video",
"path": "clips/clip_0001.mp4",
"fps": 25.0
}
],
"labels": {
"action": {
"label": "shot"
},
"attributes": {
"labels": ["left_foot"]
}
},
"metadata": {
"match_id": "match_01"
}
}
]
}
Complete Localization Example¶
{
"version": "2.0",
"date": "2026-05-19",
"task": "action_spotting",
"dataset_name": "soccer-localization-demo",
"description": "Timestamped action events.",
"modalities": ["video"],
"metadata": {
"sport": "soccer",
"split": "train"
},
"labels": {
"action": {
"type": "single_label",
"labels": ["pass", "shot", "save"]
}
},
"data": [
{
"id": "attack_0001",
"inputs": [
{
"type": "video",
"path": "clips/attack_0001.mp4",
"fps": 25.0
}
],
"events": [
{
"head": "action",
"label": "pass",
"position_ms": 1100,
"gameTime": "1 - 00:01"
},
{
"head": "action",
"label": "shot",
"position_ms": 3650,
"gameTime": "1 - 00:04"
}
]
}
]
}
Multi-Modal Tracking Example¶
{
"version": "2.0",
"date": "2026-05-19",
"task": "action_classification",
"dataset_name": "soccer-gar-multimodal-demo",
"description": "Frames and tracking inputs for one action sample.",
"modalities": ["frames_npy", "tracking_parquet"],
"labels": {
"action": {
"type": "single_label",
"labels": ["PASS", "SHOT"]
}
},
"data": [
{
"id": "train_000001",
"inputs": [
{
"type": "frames_npy",
"path": "frames_npy/train/train_000001.npy",
"fps": 2.0
},
{
"type": "tracking_parquet",
"path": "tracking_parquet/train/train_000001.parquet"
}
],
"labels": {
"action": {
"label": "PASS"
}
},
"metadata": {
"game_id": "game_001",
"position_ms": 124000,
"source_fps": 30.0,
"effective_fps": 2.0,
"window_size": 16,
"frame_interval": 15
}
}
]
}
Prediction Payloads¶
infer() returns predictions as an in-memory dictionary. It does not require
the caller to provide an output path. Use save_predictions(...) when you want
to write that dictionary to disk.
Classification prediction example:
{
"version": "2.0",
"task": "action_classification",
"date": "2026-05-19",
"metadata": {
"type": "predictions"
},
"data": [
{
"id": "clip_0001",
"labels": {
"action": {
"label": "shot",
"confidence": 0.91
}
}
}
]
}
Localization prediction example:
{
"version": "2.0",
"date": "2026-05-19",
"task": "action_spotting",
"metadata": {
"type": "predictions"
},
"data": [
{
"inputs": [
{
"type": "video",
"path": "clips/attack_0001.mp4",
"fps": 2.0
}
],
"events": [
{
"head": "action",
"label": "shot",
"frame": 73,
"position_ms": 36500,
"gameTime": "1 - 00:36",
"confidence": 0.84
}
]
}
]
}
Validation Checklist¶
datais a list.- Every supervised file has a root
labelsschema. - Classification samples use
labels.action.labelunless your code explicitly passes a different task head. - Localization samples use
events[].position_mswhenever possible. - Labels in samples/events are present in the matching root label list.
inputs[].pathresolves from the expected split root or conversionmedia_root.- Sample IDs are stable and unique, especially for evaluation.