VSTAT is a video-based benchmark for evaluating the visual state tracking capability of Multimodal Large Language Models (MLLMs). It contains 834 video clips paired with 1,500 questions whose answers cannot be inferred from any single keyframe or short segment.
| Split | Videos | Questions |
|---|---|---|
| synthetic | 450 | 550 |
| self_recorded | 80 | 100 |
| youtube | 304 | 850 |
| Total | 834 | 1,500 |
vstat_qa_clean.json β all 1,500 question-answer pairs with taxonomy labelsyoutube_metadata.json β YouTube URLs + start/end timestamps (one entry per chunk)youtube_resolutions.json β per-clip target (W, H, fps) used by the
downloader to reproduce the official release pixel layoutredactions.json β declarative privacy-redaction regions applied after trimscripts/download_youtube.py β fetches & trims the 304 YouTube clipsscripts/redact.sh β applies the privacy black-boxes from redactions.jsonscripts/build_resolution_map.py β utility to (re)build youtube_resolutions.json
from a reference rendervideos/synthetic/<category>/<id>.mp4 β Blender-rendered videos (hosted)videos/self_recorded/<category>/<id>.mp4 β author-recorded clips, hands only,
audio removed (hosted)videos/youtube/<category>/<id>.mp4 β NOT redistributed; you must
download these yourself with the provided script (see Quick start below)Pick whichever method you prefer:
# A. huggingface-cli (recommended, supports LFS)
pip install -U "huggingface_hub[cli]"
huggingface-cli download nyu-visionx/vstat \
--repo-type=dataset \
--local-dir vstat
cd vstat
# B. git clone (requires git-lfs installed)
git lfs install
git clone https://huggingface.co/datasets/nyu-visionx/vstat vstat
cd vstat
After this, you have all annotations and the synthetic + self_recorded videos. The YouTube clips are still missing β fetch them next.
The downloader reads youtube_metadata.json and downloads each source
video once with yt-dlp, then trims it into the chunks expected by
vstat_qa_clean.json. Pass --resolution-map youtube_resolutions.json
so each chunk lands at the exact (width, height, fps) of the official
release. After trimming, scripts/redact.sh applies the privacy
black-boxes (matches redactions.json) to the affected clips in place.
Important β reproducing the official release. The benchmark numbers in our paper were obtained on the clips produced by exactly this two-step pipeline (
download_youtube.py --resolution-map β¦βredact.sh). The downloader picks the smallest YouTube format that matches each clip's target dimensions and frame rate so the trim avoids any resampling drift. Skip the resolution map only for ablations on input resolution.
# Install dependencies
pip install -U yt-dlp
# macOS: brew install ffmpeg
# Ubuntu: sudo apt install ffmpeg
# 1. Fetch and trim every YouTube clip to its release-spec dims
python scripts/download_youtube.py --resolution-map youtube_resolutions.json
# 2. Apply privacy redactions in place (idempotent)
bash scripts/redact.sh
Common flags for the downloader:
# Faster: 4 parallel downloads
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --workers 4
# Test on a few videos first
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --limit 5
# Keep the full source videos around (faster re-trim, more disk)
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --keep-fulls
# Print plan without doing anything
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --dry-run
# Cap source download size (default uncapped β required for portrait sources)
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --source-cap 1080
Re-running the downloader is safe: it skips clips that already exist
on disk and writes a download_report.json listing any failures (rare,
usually due to YouTube link rot β affected clips can be reported to
the authors via the dataset issue tracker). Re-running redact.sh is
also idempotent and replaces any earlier redaction with the canonical
set defined in redactions.json.
import json
with open("vstat_qa_clean.json") as f:
data = json.load(f)
for cat, entries in data["data"].items():
for e in entries:
print(e["video_id"], e["video_path"], e["video_source"])
Each entry has these fields:
| Field | Description |
|---|---|
video_id | Unique identifier (e.g. 0001_pt1_q1) |
video_path | Relative path under videos/ |
video_source | synthetic / self_recorded / youtube |
source_task | Coarse category (e.g. basketball, dice, shell_game) |
question | Question text. For MCQ items, choices are inline (A)(B)β¦ |
answer_type | mcq or numeric |
answer | Letter (A/B/C/D) for MCQ; integer for numeric |
choices | List of MCQ option strings (empty for numeric) |
answer_index | 0-based index into choices (null for numeric) |
perceptual_complexity | List of perceptual challenge tags (see Taxonomy) |
state_element_type | count / location / attribute |
state_structure | atomic / sequence / set / dictionary |
youtube_url, youtube_id, start_time, end_time, start_sec, end_sec | Present only for video_source == "youtube" |
A minimal MCQ scoring loop (numeric questions are scored with mean relative accuracy in our paper; see Section 3.1 for details):
def score(entry, model_pred):
if entry["answer_type"] == "mcq":
return int(model_pred.strip().upper() == entry["answer"])
# numeric
try:
return int(int(model_pred) == int(entry["answer"]))
except ValueError:
return 0
Each question is annotated with:
perceptual_complexity (multi-label, paper Section 2.2):
action_ambiguity, camera_motion, homogeneity,
multi_entity_attribution, occlusion, symbolic_decodingstate_element_type (single label): count, location, attributestate_structure (single label): atomic, sequence, set, dictionaryLICENSE for full termsredact.sh step
applies black-boxes over scoreboards / on-screen text in a small
number of clips per redactions.json, matching the official release.@article{vstat2026,
title={Benchmarking Visual State Tracking in Multimodal Video Understanding},
author={Sihyun Yu and Nanye Ma and Pinzhi Huang and Hyunseok Lee and Shusheng Yang and June Suk Choi and Ellis Brown and Oscar Michel and Boyang Zheng and Jinwoo Shin and Saining Xie},
year={2026},
journal={arXiv preprint arXiv:2606.03920},
}
VSTAT is a video-based benchmark for evaluating the visual state tracking capability of Multimodal Large Language Models (MLLMs). It contains 834 video clips paired with 1,500 questions whose answers cannot be inferred from any single keyframe or short segment.
| Split | Videos | Questions |
|---|---|---|
| synthetic | 450 | 550 |
| self_recorded | 80 | 100 |
| youtube | 304 | 850 |
| Total | 834 | 1,500 |
vstat_qa_clean.json β all 1,500 question-answer pairs with taxonomy labelsyoutube_metadata.json β YouTube URLs + start/end timestamps (one entry per chunk)youtube_resolutions.json β per-clip target (W, H, fps) used by the
downloader to reproduce the official release pixel layoutredactions.json β declarative privacy-redaction regions applied after trimscripts/download_youtube.py β fetches & trims the 304 YouTube clipsscripts/redact.sh β applies the privacy black-boxes from redactions.jsonscripts/build_resolution_map.py β utility to (re)build youtube_resolutions.json
from a reference rendervideos/synthetic/<category>/<id>.mp4 β Blender-rendered videos (hosted)videos/self_recorded/<category>/<id>.mp4 β author-recorded clips, hands only,
audio removed (hosted)videos/youtube/<category>/<id>.mp4 β NOT redistributed; you must
download these yourself with the provided script (see Quick start below)Pick whichever method you prefer:
# A. huggingface-cli (recommended, supports LFS)
pip install -U "huggingface_hub[cli]"
huggingface-cli download nyu-visionx/vstat \
--repo-type=dataset \
--local-dir vstat
cd vstat
# B. git clone (requires git-lfs installed)
git lfs install
git clone https://huggingface.co/datasets/nyu-visionx/vstat vstat
cd vstat
After this, you have all annotations and the synthetic + self_recorded videos. The YouTube clips are still missing β fetch them next.
The downloader reads youtube_metadata.json and downloads each source
video once with yt-dlp, then trims it into the chunks expected by
vstat_qa_clean.json. Pass --resolution-map youtube_resolutions.json
so each chunk lands at the exact (width, height, fps) of the official
release. After trimming, scripts/redact.sh applies the privacy
black-boxes (matches redactions.json) to the affected clips in place.
Important β reproducing the official release. The benchmark numbers in our paper were obtained on the clips produced by exactly this two-step pipeline (
download_youtube.py --resolution-map β¦βredact.sh). The downloader picks the smallest YouTube format that matches each clip's target dimensions and frame rate so the trim avoids any resampling drift. Skip the resolution map only for ablations on input resolution.
# Install dependencies
pip install -U yt-dlp
# macOS: brew install ffmpeg
# Ubuntu: sudo apt install ffmpeg
# 1. Fetch and trim every YouTube clip to its release-spec dims
python scripts/download_youtube.py --resolution-map youtube_resolutions.json
# 2. Apply privacy redactions in place (idempotent)
bash scripts/redact.sh
Common flags for the downloader:
# Faster: 4 parallel downloads
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --workers 4
# Test on a few videos first
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --limit 5
# Keep the full source videos around (faster re-trim, more disk)
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --keep-fulls
# Print plan without doing anything
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --dry-run
# Cap source download size (default uncapped β required for portrait sources)
python scripts/download_youtube.py --resolution-map youtube_resolutions.json --source-cap 1080
Re-running the downloader is safe: it skips clips that already exist
on disk and writes a download_report.json listing any failures (rare,
usually due to YouTube link rot β affected clips can be reported to
the authors via the dataset issue tracker). Re-running redact.sh is
also idempotent and replaces any earlier redaction with the canonical
set defined in redactions.json.
import json
with open("vstat_qa_clean.json") as f:
data = json.load(f)
for cat, entries in data["data"].items():
for e in entries:
print(e["video_id"], e["video_path"], e["video_source"])
Each entry has these fields:
| Field | Description |
|---|---|
video_id | Unique identifier (e.g. 0001_pt1_q1) |
video_path | Relative path under videos/ |
video_source | synthetic / self_recorded / youtube |
source_task | Coarse category (e.g. basketball, dice, shell_game) |
question | Question text. For MCQ items, choices are inline (A)(B)β¦ |
answer_type | mcq or numeric |
answer | Letter (A/B/C/D) for MCQ; integer for numeric |
choices | List of MCQ option strings (empty for numeric) |
answer_index | 0-based index into choices (null for numeric) |
perceptual_complexity | List of perceptual challenge tags (see Taxonomy) |
state_element_type | count / location / attribute |
state_structure | atomic / sequence / set / dictionary |
youtube_url, youtube_id, start_time, end_time, start_sec, end_sec | Present only for video_source == "youtube" |
A minimal MCQ scoring loop (numeric questions are scored with mean relative accuracy in our paper; see Section 3.1 for details):
def score(entry, model_pred):
if entry["answer_type"] == "mcq":
return int(model_pred.strip().upper() == entry["answer"])
# numeric
try:
return int(int(model_pred) == int(entry["answer"]))
except ValueError:
return 0
Each question is annotated with:
perceptual_complexity (multi-label, paper Section 2.2):
action_ambiguity, camera_motion, homogeneity,
multi_entity_attribution, occlusion, symbolic_decodingstate_element_type (single label): count, location, attributestate_structure (single label): atomic, sequence, set, dictionaryLICENSE for full termsredact.sh step
applies black-boxes over scoreboards / on-screen text in a small
number of clips per redactions.json, matching the official release.@article{vstat2026,
title={Benchmarking Visual State Tracking in Multimodal Video Understanding},
author={Sihyun Yu and Nanye Ma and Pinzhi Huang and Hyunseok Lee and Shusheng Yang and June Suk Choi and Ellis Brown and Oscar Michel and Boyang Zheng and Jinwoo Shin and Saining Xie},
year={2026},
journal={arXiv preprint arXiv:2606.03920},
}