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. Fields for descriptions or captions may occur in interoperable OSL data, but OpenSportsLib does not currently dispatch a captioning or retrieval task pipeline.
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. |
hf_repo_id |
string | Optional | Hugging Face dataset repository recorded by the split downloader. |
hf_branch |
string | Optional | Requested branch, tag, or revision. |
hf_split |
string | Optional | Remote split containing this dataset. |
hf_format |
string | Optional | Remote storage format: json or parquet. |
hf_commit |
string | Optional | Resolved immutable commit used for the download. |
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.
The five hf_* provenance fields form the source locator for selective media
downloads. They are written together after a successful Hugging Face split
download. Consumers should require a complete locator before fetching assets;
older JSON files with only repository/branch/split metadata should be
downloaded again to acquire the format and commit pin.
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] | Interoperability metadata; not consumed by a current OpenSportsLib task route. |
dense_captions |
array[object] | Interoperability metadata; not consumed by a current OpenSportsLib task route. |
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. |
player_centroids_h5 |
tracking/live_centroids.h5 |
Columnar H5 player centroid rows keyed by timestamp_utc, with player x/y coordinates. |
player_joints_h5 |
tracking/live_joints.h5 |
Columnar H5 player joint rows keyed by timestamp_utc, with joint columns named like nose_x, nose_y, nose_z. |
fps is recommended for video and frame-array inputs. Tracking inputs can also
include fps as a fallback when timestamps are not available.
H5 player tracking inputs may include ball_path to point at a synchronized
ball H5 file. Ball files are resolved relative to the same split source_path
as path, unless an absolute path is provided. H5 loaders use timestamp_utc
as the source of truth and can clip each sample with optional
data[].metadata.start_utc and data[].metadata.end_utc values. When those
metadata fields are absent, the loader uses the available H5 timestamp range.
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"
},
{
"type": "player_centroids_h5",
"path": "tracking/live_centroids.h5",
"ball_path": "tracking/live_ball.h5"
}
]
}
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.
Open video intervals¶
A localization record may declare the playable or annotated portions of one
physical video in data[].metadata.intervals. Interval bounds are absolute
milliseconds on the physical video and follow half-open
[start_time_ms, end_time_ms) semantics:
{
"id": "sample-01",
"inputs": [{"type": "video", "path": "videos/sample.mp4"}],
"metadata": {
"annotation_status": "verified",
"intervals": [
{"period": "segment_a", "start_time_ms": 10000, "end_time_ms": 20000},
{"period": "segment_b", "start_time_ms": 30000, "end_time_ms": 40000}
]
},
"events": [
{"head": "action", "label": "Action", "position_ms": 12000}
]
}
The OpenCV localization loader exposes each interval as an ordered logical video while reading frames from the original physical file. Event timestamps are rebased to the interval start. Continuous inference state, including a test-time adapter, remains alive as the loader advances between intervals.
annotation_status controls how the logical segments are used:
verified: included in inference and evaluation;unlabeled: included in inference, but excluded from evaluation;excluded: omitted from both inference and evaluation.
Records without metadata.intervals retain full-video behavior. Bounded video
intervals currently require the OpenCV backend; selecting DALI raises an error
instead of silently reading the wrong physical frames. Events outside declared
intervals and overlapping or invalid intervals are rejected.
Caption Interoperability and VQA Payloads¶
These payloads are part of the OSL JSON ecosystem. captions and
dense_captions can round-trip through annotation tools but are not consumed
by a current OpenSportsLib task route. Q/A payloads are used by the 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
}
}
]
}
H5 Tracking Example¶
{
"version": "2.0",
"date": "2026-07-20",
"task": "action_classification",
"dataset_name": "soccer-h5-tracking-demo",
"modalities": ["player_centroids_h5"],
"labels": {
"action": {
"type": "single_label",
"labels": ["header", "other"]
}
},
"data": [
{
"id": "match_01_window_0001",
"inputs": [
{
"type": "player_centroids_h5",
"path": "live_centroids.h5",
"ball_path": "live_ball.h5"
}
],
"metadata": {
"start_utc": "2022-12-03 16:00:02.000000",
"end_utc": "2022-12-03 16:00:04.000000"
},
"labels": {
"action": {
"label": "header"
}
}
}
]
}
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. - Localization interval bounds use absolute physical-video milliseconds and
half-open
[start_time_ms, end_time_ms)semantics. - 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.