Spotter: Let the Embodied Model Lead, and the VLM Reflect for It
If our project helps you, please give us a star ⭐ on GitHub to support us.
| 📄 Paper | 🏠 Project page | 🤖 Model checkpoints | 🌐 Simulator | 🚀 Get started |
|---|---|---|---|---|
| arXiv | zqc3117.github.io/Spotter | pi0.5 · Cosmos Policy | RoboCasa | Installation · Quick start |
Spotter adds a lightweight intervention layer between a frozen embodied model (System 1) and a VLM (System 2). Instead of having the VLM plan every step, the embodied model leads and keeps executing, while a local screener watches in parallel and the VLM steps in only to confirm and repair an error. The VLM's job becomes much simpler, so the whole framework works with a locally served, open-source Qwen and no privileged information.
🎬 Demos
Real robot (Franka Research 3), stacking the blue cup onto the pink cup. Shown at 2×, with the judge's thinking time shortened; the on-screen clock is real time.
Watch the full video on the project page.
🔥 Highlights
- A lightweight intervention layer. Like a person who keeps a little attention on a task and focuses only when something looks wrong, Spotter lets execution flow continuously and brings in the VLM only on a confirmed error.
- A simpler job for the VLM. The VLM no longer drives the robot. It only has to notice a failure, repair it with a short primitive plan, and check the fix. A local Qwen with no access to the task's success signal is enough.
- Continuous execution, low overhead. The embodied model never waits for the VLM on the routine path: a successful episode takes only 13–16 s longer than the policy alone.
💡 Why Spotter
Existing VLM-led robot agents are serial: the VLM plans a segment, the embodied model executes it, and the VLM decides what comes next. This forces a dilemma:
- Short segments mean frequent VLM calls and high latency, even on easy tasks.
- Long segments mean a deviation during execution cannot be caught in time, and later actions keep going wrong.
Spotter removes this trade-off by supervising in parallel rather than in series.
| VLM-led agents | Spotter | |
|---|---|---|
| Who drives execution | VLM plans every step | Embodied model leads |
| When the VLM is called | Before every step | Only on a confirmed error |
| Embodied model waits for the VLM | Every step | Only during a repair |
| Needs privileged information | Usually (e.g. the success signal) | No |
| Works with a local Qwen | Only with privileged information | Yes |
🎯 Works with a weaker model, without privileged information
With the same local Qwen and no privileged information on RoboCasa (1,200 episodes), a representative VLM-led agent falls below the policy alone (Cosmos Policy 67.9 → 65.2, π0.5 64.3 → 60.5), while Spotter still improves it (→ 71.7 and 68.4).
⚡ Almost no overhead when nothing goes wrong
With Qwen, the judge is called less than once per successful episode on average, which adds only 13–16 s over the policy alone and takes about 70% less time than a VLM-led agent using the same Qwen.
Status (October 2026): please use RoboTwin 2.0 for now. The RoboCasa path has a rendering bug: in a small fraction of episodes the offscreen renderer returns black or noisy camera frames, which makes those episodes fail. A fix is being validated. Until it lands, the RoboCasa instructions in this README are hidden; RoboTwin 2.0 is unaffected and fully supported. Run it with one command,
bash robotwin_eval.sh(see Evaluation).RoboTwin 2.0 results and full logs (control vs. Spotter-Qwen, every episode's frames, prompts and replies): 50 tasks × 10 episodes.
📑 Index
- File Structure
- Environment Setup
- Model Preparation
- Simulator Assets and Episode Sets
- Evaluation with Released Checkpoints: RoboTwin 2.0
- Plugging In Your Own Embodied Model
- Outputs · Configuration Reference
- Acknowledgements · License · Citation
📂 File Structure
Spotter/
├── spotter.sh # one entry point: serve Qwen / the policy, run episodes, summarize
├── robotwin_eval.sh # one-command RoboTwin 2.0 evaluation (servers + Spotter arm + control + summary)
├── recovery_explore/ # Spotter itself
│ ├── judge_driver_v7.sh # supervision loop for one lane: screen -> judge -> repair -> verify
│ ├── env_service.py # RoboCasa simulation service (RPC), started by launch_svc.sh
│ ├── env_service_robotwin.py # RoboTwin 2.0 simulation service (same RPC surface, SAPIEN)
│ ├── robotwin_intervention.py # RoboTwin repair primitives (dual arm); robodojo_intervention.py is its base
│ ├── robotwin_policy_server.py # RoboTwin pi0.5 policy server (openpi websocket protocol)
│ ├── primitives.py # motion primitives the judge composes repairs from
│ ├── JUDGE_BRIEF_V7.md # judge prompt; BRIEF_PNP.md / BRIEF_MECH.md / BRIEF_RT.md add task-family notes
│ ├── cli/ # screener, judge channels (Qwen / GPT), harness, few-shot tools
│ ├── memory_bank/ # frozen library of 77 general lessons (RoboCasa)
│ ├── memory_bank_robotwin_noreset/ # the RoboTwin 2.0 lessons
│ ├── episode_sets/ # sall500.txt (paper, 1,200 episodes) · s500q96.txt (96, debugging) · rt50x10n.txt (RoboTwin 2.0, 500)
│ ├── fewshot_bank/ # (downloaded) worked examples for the 1-shot setting; robotwin/ for RoboTwin 2.0
│ └── runs_<RUN>_<family>/ # (generated) results.jsonl, per-lane logs, frames shown to the judge
├── rpc/ # π0.5 policy server, RPC protocol, RoboCasa rollout and collection scripts
├── cf_bench/ # snapshot and replay tools for the error-correction study
├── env/ # env.sh.example, requirements-robocasa.txt, requirements-pi05.txt
├── assets/ # logo
└── LICENSE · THIRD_PARTY_NOTICES.md · LICENSES/
🔧 Environment Setup
Qwen screener and judge. vLLM in its own environment, and a local env/env.sh:
python3 -m venv .venvs/vllm && .venvs/vllm/bin/pip install vllm cp env/env.sh.example env/env.sh && $EDITOR env/env.sh
RoboTwin 2.0. Spotter runs on RoboTwin 2.0
(aloha-agilex dual arm, SAPIEN 3, 50 tasks, demo_randomized scenes); the family is robotwin.
RoboTwin's simulator and planner (SAPIEN 3, mplib, cuRobo) live in their own Python 3.10 environment,
laid out as a portable runtime that env/env.sh points at:
<ROBOTWIN_RUNTIME>/
├── .venv/ # python 3.10: sapien 3.0.0b1, mplib 0.2.1, toppra, torch (CUDA build matching your GPU)
└── worktree/third_party/RoboTwin/ # the RoboTwin 2.0 tree
├── envs/curobo/src/ # cuRobo sources (its CUDA kernels must match your torch)
├── assets/{objects,background_texture,embodiments}/ # RoboTwin 2.0 assets (TianxingChen/RoboTwin2.0 on the Hub)
└── policy/pi0/checkpoints/pi05_robotwin2_clean_randomized/robotwin2/40000/ # the pi0.5 checkpoint
Install RoboTwin 2.0 and its assets as its README says, then set in env/env.sh:
export ROBOTWIN_RUNTIME=/path/to/robotwin-runtime # or ROBOTWIN_ROOT + ROBOTWIN_PY export ROBOTWIN_POLICY_PYTHON=/path/to/robotwin-policy-venv/bin/python # a venv satisfying policy/pi05/requirements.txt (openpi, lerobot < 0.3)
The simulation renders with SAPIEN's ray tracer (Vulkan, RTX-class GPU); on RTX 5090 the optix denoiser is used
because the default oidn fails there.
📦 Model Preparation
1) Qwen judge and screener.
huggingface-cli download Qwen/Qwen3.8-27B-FP8 --local-dir checkpoints/Qwen3.8-27B-FP8
Then set in env/env.sh:
export QWEN_MODEL=$PWD/checkpoints/Qwen3.8-27B-FP8 export VLLM_VENV=$PWD/.venvs/vllm
2) RoboTwin 2.0. The policy is the pi0.5 checkpoint
motus-robotics/pi0.5_robotwin2, placed at
policy/pi0/checkpoints/pi05_robotwin2_clean_randomized/robotwin2/40000/ inside the RoboTwin tree
(see Environment Setup). For the 1-shot setting, download the RoboTwin example bank (33 MB)
into recovery_explore/fewshot_bank/robotwin/:
curl -L https://github.com/zqc3117/Spotter/releases/download/v1.0/fewshot_bank_robotwin.tar.gz | tar -xz -C recovery_exploreGPT judge (optional). Set
ENGINE=apitogether withOPENAI_RESPONSES_URLandOPENAI_API_TOKEN_FILE; the judge becomes GPT-6 Astra while the screener stays the local Qwen.
🧭 Simulator Assets and Episode Sets
RoboTwin 2.0 assets are installed with RoboTwin itself (Environment Setup).
Episode sets ship with the repository, one TASK:SEED:EPISODE per line:
| File | Episodes | Use |
|---|---|---|
episode_sets/rt50x10n.txt |
500 | RoboTwin 2.0: 50 tasks × 10 episodes |
episode_sets/rt500.txt |
500 | RoboTwin 2.0 evaluation used for the released results: rt50x10n with the episodes that cannot be built in the current simulator replaced by the next buildable episode of the same task |
🚀 Evaluation with Released Checkpoints
Each server runs in its own terminal after source env/env.sh.
1) Serve the Qwen screener and judge (one replica per GPU, ports 8301, 8302, ...).
bash spotter.sh judge --gpus 0,1,2,3
curl -s http://127.0.0.1:8301/v1/models # checkWith fewer GPUs, pass fewer ids (e.g. --gpus 0) and export QWEN_PORTS=8301 before running.
RoboTwin 2.0
One command. robotwin_eval.sh starts Qwen and the pi0.5 policy if they are not already up, runs the Spotter arm
and then the policy-only control on the same 500 episodes (rt500), and prints the summary. It uses the settings of
the released results: 1.8× step budget, 1-shot, Qwen thinking in the repair turns with QWEN_THINK_BUDGET=5000.
bash robotwin_eval.sh --judge-gpus 0,1,2,3 --policy-gpu 4 --lanes 12
Add --dry-run to print the commands without running them. Each RoboTwin simulator takes about 8 GB of GPU memory,
so plan about three lanes per 32 GB GPU. The judge's thinking dominates the time of the Spotter arm; more Qwen replicas
(--judge-gpus) make it faster.
Step by step
With the Qwen servers from step 1 running:
bash spotter.sh policy robotwin --gpu 4 # pi0.5 for RoboTwin, port 9100 (check: curl http://127.0.0.1:9100/healthz) bash spotter.sh run robotwin # smoke: adjust_bottle, episode 35 bash spotter.sh run robotwin rt500 --lanes 12 --one-shot # Spotter arm on the 500 evaluation episodes (1-shot) bash spotter.sh run robotwin rt500 --lanes 12 --arm ctrl # the policy-only baseline on the same episodes bash spotter.sh summary
Results with this code. We re-ran the Spotter arms on RoboTwin 2.0 with this version of the code, on the 500
episodes of rt500 (1.8× step budget, 1-shot); the control is the policy alone on the same 500 episodes:
| Arm | Success |
|---|---|
| Policy alone (pi0.5) | 240 / 500 (48.0%) |
Spotter-Qwen, QWEN_THINK_BUDGET=2000 |
252 / 500 (50.4%) |
Spotter-Qwen, QWEN_THINK_BUDGET=5000 (default) |
267 / 500 (53.4%) |
Because every run involves randomness, these numbers differ somewhat from the paper. We have uploaded the per-episode results, with every episode's frames, judge prompts and replies, to ModelScope: 50 tasks × 10 episodes.
Run options (bash spotter.sh run <family> [SET] [options]):
| Option | Meaning |
|---|---|
SET |
smoke (1 episode, default), rt500 or rt50x10n (RoboTwin 2.0, 500 episodes), or a file with one TASK:SEED:EPISODE per line |
--lanes N |
episodes run in parallel; lane N runs its own simulation service on port 8450+N |
--sim-gpus N |
GPUs the simulation services share, GPU 0..N-1 (default: all) |
--arm |
both (control and Spotter on each episode, default), treat (Spotter only), ctrl (policy only) |
--one-shot |
add one worked example to the judge (needs the example bank) |
--run NAME |
run name (default <family>_<set>); rerunning the same name resumes it |
--dry-run |
print the commands without running them |
Both arms of an episode share the same scene, instruction and step budget:
| Arm | Description |
|---|---|
| Control | Policy only; no judge calls. |
| Treatment | Policy plus judge and optional interventions. |
spotter.sh run robotwin applies the RoboTwin column of the Configuration Reference; every setting can still be overridden through the environment.
🔌 Plugging In Your Own Embodied Model
Spotter never trains the policy, so a new embodied model only needs to be served and registered:
- Serve it over the same RPC interface as
rpc/policy_server_pi05.py:GET /health, andPOST /v1/infertaking anInferenceRequest(task description, seed, observation images and state) and returning anInferenceResponsewith an action chunk (seerpc/protocol.py). - Register the family next to
cosmosandpi05: action dimension and steps per chunk inrecovery_explore/cosmos_call.pyandenv_service.py, service launch inlaunch_svc.sh, port and defaults inspotter.sh. - Choose the supervision granularity:
WINDOW(chunks per screening check) andTEL_CHUNKS(telemetry rows shown to the judge). - Run
bash spotter.sh run <your-family> ....
📈 Outputs
recovery_explore/runs_<RUN>_<family>/
├── results.jsonl # one line per episode
├── lane<N>/driver.log # per-lane progress
└── lane<N>/jd-.../w<k>/ # frames shown to the judge in window k
| Field | Meaning |
|---|---|
task, seed, episode |
the episode |
control_success / treatment_success |
success of the policy alone / with Spotter |
interventions, judge_calls, screened_windows |
repairs executed, judge calls, windows the screener passed |
ctrl_s, treat_s |
wall-clock seconds of the control and Spotter arms (-1 if the arm was skipped) |
model_s, screen_s |
seconds spent in judge calls and in screener calls |
stale_steps, stale_ivs |
asynchronous mode only: steps and repairs made on an out-of-date window |
bash spotter.sh summary # all runs
bash spotter.sh summary robotwin_rt50x10n🧩 Configuration Reference
| Setting | RoboTwin 2.0 · pi0.5 |
|---|---|
FAMILY_OVERRIDE |
robotwin |
| Steps per chunk | 16 |
WINDOW (judge window, in chunks) |
2 |
RT_TEL_CHUNKS |
10 |
STEP_BUDGET_SCALE (same for control and Spotter) |
1.8 |
SCREEN / SCREEN_CONFIRM2 |
1 / 1 |
MAX_INTERVENTIONS |
5 |
ALLOW_RESET / MAX_RESETS (no rollbacks) |
0 / 0 |
ENGINE / MODEL |
qwen / qwen38 |
QWEN_KEEP_TURNS / COMPACT_AT |
10 / 40000 |
FEWSHOT |
1 (--one-shot, needs the RoboTwin example bank) |
QWEN_THINK_BUDGET (repair turns) |
5000 |
| Policy port / server | 9100 / recovery_explore/robotwin_policy_server.py |
spotter.sh run robotwin and robotwin_eval.sh apply these values; every one can be overridden through the environment.
Qwen thinks only in the repair turns of an intervention, up to QWEN_THINK_BUDGET (default 5000) tokens per call; the screener and the window verdict never think, and QWEN_REPAIR_THINK=0 turns thinking off everywhere, as in the paper.
Choosing the thinking budget. Set QWEN_THINK_BUDGET to what your Qwen servers can afford. A larger budget gives
the repair plan more room but makes every repair turn slower; a smaller one is faster but cuts the thinking off before
it finishes. Use the largest budget that keeps repair turns within ACT_DEADLINE (raise that too on slow GPUs or with
many lanes per replica), and check after a run that most repair turns were not cut off. Each repair reply is saved
as act_w*_a*_r*.json in the episode folder, with think_budget_hit: true when the thinking ran into the budget:
python3 - <<'EOF' import glob, json files = glob.glob("recovery_explore/runs_<RUN>_<family>/lane*/jd-*/act_w*.json") hit = sum(bool(json.load(open(f)).get("think_budget_hit")) for f in files) print(f"{hit}/{len(files)} repair turns hit the thinking budget") EOF
If more than about half are cut, raise the budget (or add Qwen replicas so the larger budget stays fast). On RoboTwin 2.0, a budget of 2000 cut off the thinking in about 65% of the repair turns, and 5000 in about 44%.
How the judge is paced has its own switches, all off or conservative by default:
| Variable | Default | Effect |
|---|---|---|
ASYNC_SCREEN |
0 |
1 lets the policy keep stepping while the judge works; the loop then resumes at the newest finished window. Faster, but the judge is looking at a window the episode has already moved past — results.jsonl records that cost as stale_steps and stale_ivs. 2 also holds the policy within ASYNC_K windows of the last check. |
MISS_GRACE |
2 |
Windows the policy gets to retry on its own after closing on nothing, before the judge may take over. |
FIRST_IV_FRAC |
35 |
The first intervention waits until the policy has closed on nothing at least once, or the episode is this far (%) into its step budget. 0 disables the gate. |
MONITOR_AFTER_BUDGET |
1 |
Keep calling the judge once the intervention budget is spent, for the record. 0 stops calling it. |
Changing STEP_BUDGET_SCALE, WINDOW or the policy family changes the experiment; compare runs by matched
task/seed/episode, not only by aggregate success rate.
🙏 Acknowledgements
- RoboCasa: the simulation benchmark and assets we evaluate on.
- Cosmos Policy: the released RoboCasa checkpoint and its policy server.
- π0.5 and LeRobot: the RoboCasa π0.5 checkpoint and the library that loads it.
- Qwen and vLLM: the local screener and judge.
- RPent: parts of
recovery_explore/are adapted from it (Apache-2.0, seeTHIRD_PARTY_NOTICES.md).
📜 License
This project is released under the MIT License. Some files in
recovery_explore/ are adapted from RPent and remain under the Apache License 2.0; see
THIRD_PARTY_NOTICES.md.
📖 Citation
@article{li2026spotter, title = {Spotter: Let the Embodied Model Lead, and the VLM Reflect for It}, author = {Li, Long and Zhao, Qichao and Yang, Yue and Xu, Fan and Wang, Zhe and Liew, Alan Wee-Chung and Qu, Chao and Shen, Heng Tao and Pan, Shirui}, journal = {arXiv preprint arXiv:2609.36808}, year = {2026} }
