Benchmark annotations for PRIMO R1 (paper): task-progress estimation and failure detection for long-horizon robotic manipulation, across four environments with in-domain and out-of-domain splits.
23,704 samples, 7 splits. This repo holds annotations only (71 MB); videos are in primo-video-media.
| Split | DatasetLoader name | Samples | Video group needed |
|---|---|---|---|
agibot-id | primo-bench-id-agibot | 3,000 | agibot (349 GB) |
agibot-ood | primo-bench-ood-agibot | 3,000 | agibot (349 GB) |
behavior-1k-id | primo-bench-id-behavior-1k | 3,999 | behavior-1k (5,981 GB) |
behavior-1k-ood | primo-bench-ood-behavior-1k | 3,000 | behavior-1k (5,981 GB) |
robotwin-id | primo-bench-id-robotwin | 3,000 | robotwin (12.5 GB) |
robotwin-ood | primo-bench-ood-robotwin | 2,914 | robotwin (12.5 GB) |
real-humanoid-ood | primo-bench-ood-real-humanoid | 4,791 | real-humanoid (27.6 GB) |
real-humanoid-ood is the cross-environment generalization split β real hardware, never seen in training. It is where SFT-only models fall off most sharply.
Start with robotwin. At 12.5 GB of video it is by far the cheapest complete split, and it covers both ID and OOD. behavior-1k needs ~6 TB of video; do not reach for it first.
{
"problem": "Task info:\nSort in the warehouse\n\nInit Scene:\nA robot stands in front of a table ...",
"problem_type": "numerical", # numerical | regression | multiple choice | boolean | free-form
"data_type": "video",
"path": "./agibot/observation/359/687696/20.0/head_color.mp4",
"solution": "<answer>20.0</answer>", # ground truth, same tag format the model must emit
"options": [], # non-empty only for multiple choice
"done_actions": ["Retrieve wet wipes from left material frame with the left arm."],
"meta_data": {"episode_id": 687696, "task_id": 359, "task_name": "Sort in the warehouse"},
"init_frame_path": "./primo-video/agibot/frames/observation/359/687696/20.0/head_color_init.jpg",
"current_frame_path": "./primo-video/agibot/frames/observation/359/687696/20.0/head_color_current.jpg"
}
Progress targets are percentages on a 0β100 scale. All media paths are relative and resolve against your local data root.
from datasets import load_dataset
ds = load_dataset("LeonOverload/primo-bench-json", "robotwin-ood", split="train")
print(ds[0]["problem"], ds[0]["solution"])
Good for inspecting the data. To actually run an evaluation, use raw_json/ as below β the eval harness reads those files, not the JSONL shards.
git clone https://github.com/10-OASIS-01/PRIMO-R1 && cd PRIMO-R1
conda create -n primo-r1 python=3.11 && conda activate primo-r1
bash setup.sh
export VIDEO_DATA_ROOT=/path/to/PRIMO-Data
export MODEL_ROOT=/path/to/models
The vendored transformers-main/ tree that setup.sh installs last is required β installing a PyPI transformers over it is the usual cause of shape and processor errors.
Download raw_json/ straight into $VIDEO_DATA_ROOT, which reproduces the layout the dataset registry expects (primo-bench/<source>/{id,ood}.json):
hf download LeonOverload/primo-bench-json --repo-type dataset \
--include "raw_json/*" --local-dir /tmp/primo-bench
mkdir -p "$VIDEO_DATA_ROOT"
cp -r /tmp/primo-bench/raw_json/primo-bench "$VIDEO_DATA_ROOT/"
Pull only the group your split needs β see the table above. For robotwin:
hf download LeonOverload/primo-video-media --repo-type dataset \
--include "robotwin.z*" --local-dir /tmp/primo-video-zips
That one pattern matches all parts including the final .zip. Reassemble and extract into $VIDEO_DATA_ROOT/primo-video/:
cd /tmp/primo-video-zips
7z x robotwin.zip -o"$VIDEO_DATA_ROOT/primo-video/"
# without 7-Zip:
# zip -FF robotwin.zip --out robotwin_fixed.zip
# unzip robotwin_fixed.zip -d "$VIDEO_DATA_ROOT/primo-video/"
The archives already contain the pre-extracted anchor frames, so there is no preprocessing step. The resulting tree:
$VIDEO_DATA_ROOT/
βββ primo-bench/robotwin/{id,ood}.json
βββ primo-video/robotwin/
βββ videos/...
βββ frames/...
hf download LeonOverload/PRIMO-R1-7B --local-dir "$MODEL_ROOT/PRIMO-R1-7B"
Models and splits are selected by editing a comment-toggled heredoc in the launcher β there are no CLI flags for them:
# src/eval/src/eval_interleave_local.sh
model_paths=$(cat <<EOF | grep -v '^#' | grep -v '^$'
$MODEL_ROOT/PRIMO-R1-7B
# $MODEL_ROOT/PRIMO-COT-SFT-7B
EOF
)
file_names=$(cat <<EOF | grep -v '^#' | grep -v '^$'
primo-bench-ood-robotwin
# primo-bench-id-robotwin
# primo-bench-ood-real-humanoid
EOF
)
bash src/eval/src/eval_interleave_local.sh
Or call the harness directly:
python src/eval/eval_interleave.py \
--model_path "$MODEL_ROOT/PRIMO-R1-7B" \
--file_name primo-bench-ood-robotwin \
--output_path src/r1-v/eval_outputs/interleave/PRIMO-R1-7B/primo-bench-ood-robotwin.json \
--sample_size 50 # smoke test; drop or set 0 for the full split
Results land in src/r1-v/eval_outputs/interleave/<model>/<split>.json, with per-sample records under results and a summary under final_acc. Runs resume from an existing output file, so an interrupted eval continues where it stopped.
Use eval_interleave_local.sh for PRIMO R1 models. eval_baseline_local.sh is for video-only baselines (Qwen2.5-VL, Cosmos-Reason1, RoboBrain, ProgressLM, InternVL 3.5), and eval_api.sh for OpenAI-compatible endpoints.
Answers are scored per problem_type: multiple choice and boolean by exact match, free-form by mean ROUGE-1/2/L F-measure, numerical and regression by relative accuracy.
The harnesses score regression with three different formulas, and they are not comparable. Each output file records which one it used in its regression_metric field:
| Harness | regression_metric | Formula |
|---|---|---|
eval_interleave.py | linear_relative_accuracy | 1 - abs(pred-gt)/abs(gt), clipped to [0,1] |
eval_local.py, eval_api.py, eval_internvl.py | threshold_relative_accuracy | fraction of thresholds t β [0.5, 0.95] step 0.05 where relative error < 1-t |
eval_ablation_modality.py | absolute_range_accuracy | 1 - abs(pred-gt)/100 against a fixed 0β100 range |
The published PRIMO R1 numbers come from linear_relative_accuracy. The threshold variant is stricter and takes only 10 discrete values per sample. Check the field before putting two numbers in the same table.
Decoding follows the Qwen2.5-VL demo: top_p=0.001, temperature=0.01. Larger top_p produces garbled output. The published results used an effective cap of 22 frames per video.
Progress estimation averaged over four environments (paper Table 1):
| Model | Avg MRA β | Avg MAE β |
|---|---|---|
| GPT-4o | 79.33 | 20.67 |
| Qwen2.5-VL-72B | 73.80 | 23.80 |
| Qwen2.5-VL-7B | 67.79 | 29.99 |
| ProgressLM | 78.32 | 20.87 |
| VLAC | 74.90 | 25.10 |
| PRIMO R1 (7B) | 82.90 | 15.52 |
| Code | 10-OASIS-01/PRIMO-R1 |
| Collection | PRIMO R1 |
| Paper | arXiv 2603.15600 Β· project page |
| Models | PRIMO-R1-7B Β· PRIMO-COT-SFT-7B |
| Videos | primo-video-media |
| Training data | primo-sft-json Β· primo-rl-json |
raw_json/ β the files the eval harness reads, in release layoutjsonl/ β flattened shards for the Dataset Viewerjsonl_subsets/ β per-split shards backing the viewer's subset selectorsummary.json β row and shard counts generated at build timeThe authoritative split registry is the cfg dict in src/r1-v/src/open_r1/DatasetLoader.py; add new splits there rather than passing raw paths through scripts.
If you find our work helpful for your research, please consider citing our work.
@misc{liu2026passiveobserveractivecritic,
title={From Passive Observer to Active Critic: Reinforcement Learning Elicits Process Reasoning for Robotic Manipulation},
author={Yibin Liu and Yaxing Lyu and Daqi Gao and Zhixuan Liang and Weiliang Tang and Shilong Mu and Xiaokang Yang and Yao Mu},
year={2026},
eprint={2603.15600},
archivePrefix={arXiv},
primaryClass={cs.RO},
url={https://arxiv.org/abs/2603.15600},
}
5 commits
Benchmark annotations for PRIMO R1 (paper): task-progress estimation and failure detection for long-horizon robotic manipulation, across four environments with in-domain and out-of-domain splits.
23,704 samples, 7 splits. This repo holds annotations only (71 MB); videos are in primo-video-media.
| Split | DatasetLoader name | Samples | Video group needed |
|---|---|---|---|
agibot-id | primo-bench-id-agibot | 3,000 | agibot (349 GB) |
agibot-ood | primo-bench-ood-agibot | 3,000 | agibot (349 GB) |
behavior-1k-id | primo-bench-id-behavior-1k | 3,999 | behavior-1k (5,981 GB) |
behavior-1k-ood | primo-bench-ood-behavior-1k | 3,000 | behavior-1k (5,981 GB) |
robotwin-id | primo-bench-id-robotwin | 3,000 | robotwin (12.5 GB) |
robotwin-ood | primo-bench-ood-robotwin | 2,914 | robotwin (12.5 GB) |
real-humanoid-ood | primo-bench-ood-real-humanoid | 4,791 | real-humanoid (27.6 GB) |
real-humanoid-ood is the cross-environment generalization split β real hardware, never seen in training. It is where SFT-only models fall off most sharply.
Start with robotwin. At 12.5 GB of video it is by far the cheapest complete split, and it covers both ID and OOD. behavior-1k needs ~6 TB of video; do not reach for it first.
{
"problem": "Task info:\nSort in the warehouse\n\nInit Scene:\nA robot stands in front of a table ...",
"problem_type": "numerical", # numerical | regression | multiple choice | boolean | free-form
"data_type": "video",
"path": "./agibot/observation/359/687696/20.0/head_color.mp4",
"solution": "<answer>20.0</answer>", # ground truth, same tag format the model must emit
"options": [], # non-empty only for multiple choice
"done_actions": ["Retrieve wet wipes from left material frame with the left arm."],
"meta_data": {"episode_id": 687696, "task_id": 359, "task_name": "Sort in the warehouse"},
"init_frame_path": "./primo-video/agibot/frames/observation/359/687696/20.0/head_color_init.jpg",
"current_frame_path": "./primo-video/agibot/frames/observation/359/687696/20.0/head_color_current.jpg"
}
Progress targets are percentages on a 0β100 scale. All media paths are relative and resolve against your local data root.
from datasets import load_dataset
ds = load_dataset("LeonOverload/primo-bench-json", "robotwin-ood", split="train")
print(ds[0]["problem"], ds[0]["solution"])
Good for inspecting the data. To actually run an evaluation, use raw_json/ as below β the eval harness reads those files, not the JSONL shards.
git clone https://github.com/10-OASIS-01/PRIMO-R1 && cd PRIMO-R1
conda create -n primo-r1 python=3.11 && conda activate primo-r1
bash setup.sh
export VIDEO_DATA_ROOT=/path/to/PRIMO-Data
export MODEL_ROOT=/path/to/models
The vendored transformers-main/ tree that setup.sh installs last is required β installing a PyPI transformers over it is the usual cause of shape and processor errors.
Download raw_json/ straight into $VIDEO_DATA_ROOT, which reproduces the layout the dataset registry expects (primo-bench/<source>/{id,ood}.json):
hf download LeonOverload/primo-bench-json --repo-type dataset \
--include "raw_json/*" --local-dir /tmp/primo-bench
mkdir -p "$VIDEO_DATA_ROOT"
cp -r /tmp/primo-bench/raw_json/primo-bench "$VIDEO_DATA_ROOT/"
Pull only the group your split needs β see the table above. For robotwin:
hf download LeonOverload/primo-video-media --repo-type dataset \
--include "robotwin.z*" --local-dir /tmp/primo-video-zips
That one pattern matches all parts including the final .zip. Reassemble and extract into $VIDEO_DATA_ROOT/primo-video/:
cd /tmp/primo-video-zips
7z x robotwin.zip -o"$VIDEO_DATA_ROOT/primo-video/"
# without 7-Zip:
# zip -FF robotwin.zip --out robotwin_fixed.zip
# unzip robotwin_fixed.zip -d "$VIDEO_DATA_ROOT/primo-video/"
The archives already contain the pre-extracted anchor frames, so there is no preprocessing step. The resulting tree:
$VIDEO_DATA_ROOT/
βββ primo-bench/robotwin/{id,ood}.json
βββ primo-video/robotwin/
βββ videos/...
βββ frames/...
hf download LeonOverload/PRIMO-R1-7B --local-dir "$MODEL_ROOT/PRIMO-R1-7B"
Models and splits are selected by editing a comment-toggled heredoc in the launcher β there are no CLI flags for them:
# src/eval/src/eval_interleave_local.sh
model_paths=$(cat <<EOF | grep -v '^#' | grep -v '^$'
$MODEL_ROOT/PRIMO-R1-7B
# $MODEL_ROOT/PRIMO-COT-SFT-7B
EOF
)
file_names=$(cat <<EOF | grep -v '^#' | grep -v '^$'
primo-bench-ood-robotwin
# primo-bench-id-robotwin
# primo-bench-ood-real-humanoid
EOF
)
bash src/eval/src/eval_interleave_local.sh
Or call the harness directly:
python src/eval/eval_interleave.py \
--model_path "$MODEL_ROOT/PRIMO-R1-7B" \
--file_name primo-bench-ood-robotwin \
--output_path src/r1-v/eval_outputs/interleave/PRIMO-R1-7B/primo-bench-ood-robotwin.json \
--sample_size 50 # smoke test; drop or set 0 for the full split
Results land in src/r1-v/eval_outputs/interleave/<model>/<split>.json, with per-sample records under results and a summary under final_acc. Runs resume from an existing output file, so an interrupted eval continues where it stopped.
Use eval_interleave_local.sh for PRIMO R1 models. eval_baseline_local.sh is for video-only baselines (Qwen2.5-VL, Cosmos-Reason1, RoboBrain, ProgressLM, InternVL 3.5), and eval_api.sh for OpenAI-compatible endpoints.
Answers are scored per problem_type: multiple choice and boolean by exact match, free-form by mean ROUGE-1/2/L F-measure, numerical and regression by relative accuracy.
The harnesses score regression with three different formulas, and they are not comparable. Each output file records which one it used in its regression_metric field:
| Harness | regression_metric | Formula |
|---|---|---|
eval_interleave.py | linear_relative_accuracy | 1 - abs(pred-gt)/abs(gt), clipped to [0,1] |
eval_local.py, eval_api.py, eval_internvl.py | threshold_relative_accuracy | fraction of thresholds t β [0.5, 0.95] step 0.05 where relative error < 1-t |
eval_ablation_modality.py | absolute_range_accuracy | 1 - abs(pred-gt)/100 against a fixed 0β100 range |
The published PRIMO R1 numbers come from linear_relative_accuracy. The threshold variant is stricter and takes only 10 discrete values per sample. Check the field before putting two numbers in the same table.
Decoding follows the Qwen2.5-VL demo: top_p=0.001, temperature=0.01. Larger top_p produces garbled output. The published results used an effective cap of 22 frames per video.
Progress estimation averaged over four environments (paper Table 1):
| Model | Avg MRA β | Avg MAE β |
|---|---|---|
| GPT-4o | 79.33 | 20.67 |
| Qwen2.5-VL-72B | 73.80 | 23.80 |
| Qwen2.5-VL-7B | 67.79 | 29.99 |
| ProgressLM | 78.32 | 20.87 |
| VLAC | 74.90 | 25.10 |
| PRIMO R1 (7B) | 82.90 | 15.52 |
| Code | 10-OASIS-01/PRIMO-R1 |
| Collection | PRIMO R1 |
| Paper | arXiv 2603.15600 Β· project page |
| Models | PRIMO-R1-7B Β· PRIMO-COT-SFT-7B |
| Videos | primo-video-media |
| Training data | primo-sft-json Β· primo-rl-json |
raw_json/ β the files the eval harness reads, in release layoutjsonl/ β flattened shards for the Dataset Viewerjsonl_subsets/ β per-split shards backing the viewer's subset selectorsummary.json β row and shard counts generated at build timeThe authoritative split registry is the cfg dict in src/r1-v/src/open_r1/DatasetLoader.py; add new splits there rather than passing raw paths through scripts.
If you find our work helpful for your research, please consider citing our work.
@misc{liu2026passiveobserveractivecritic,
title={From Passive Observer to Active Critic: Reinforcement Learning Elicits Process Reasoning for Robotic Manipulation},
author={Yibin Liu and Yaxing Lyu and Daqi Gao and Zhixuan Liang and Weiliang Tang and Shilong Mu and Xiaokang Yang and Yao Mu},
year={2026},
eprint={2603.15600},
archivePrefix={arXiv},
primaryClass={cs.RO},
url={https://arxiv.org/abs/2603.15600},
}
5 commits