# Pipeline Anotasi

`lerobot-annotate` memantau video setiap episode dengan model bahasa-visi (VLM) dan menulis anotasi bahasa alami kembali ke dataset Anda. Alat ini mengisi dua kolom bahasa dari halaman [Kolom Bahasa dan Resep](./language_and_recipes) — `language_persistent` dan `language_events` — langsung ke `data/chunk-*/file-*.parquet`.

Singkatnya: arahkan ke dataset LeRobot, dan ia menambahkan subtask, rencana, memori, interupsi, ucapan, serta visual Q&A yang dapat digunakan untuk melatih kebijakan.

## Bagaimana semuanya saling terhubung

```text
  your dataset                  lerobot-annotate
  (LeRobotDataset v3.0)
        │
        ▼
  ┌─────────────────────────────────────────────────────┐
  │                    read episodes                     │
  └──────────────────────────┬──────────────────────────┘
                             │
        ┌────────────────────┼────────────────────┐
        ▼                    ▼                     ▼
  ┌──────────┐      ┌───────────────┐        ┌──────────┐       one shared Qwen-VL
  │   plan   │      │ interjections │        │   vqa    │  ◀──   server (vLLM, OpenAI
  └────┬─────┘      └───────┬───────┘        └────┬─────┘        API) drives all three
       └────────────────────┼─────────────────────┘
                            │   each module stages raw JSONL
                            ▼   into .annotate_staging/
                  ┌─────────────────┐
                  │    validator    │  ◀──  checks everything
                  └────────┬────────┘
                           ▼
                  ┌─────────────────┐
                  │     writer      │
                  └────────┬────────┘
                           ▼
              data/chunk-*/file-*.parquet
              (+ meta/info.json tools)
```

Tiga modul (`plan`, `interjections`, `vqa`) semuanya berkomunikasi dengan **satu** VLM yang sama. Setiap modul menyimpan outputnya ke disk, validator memvalidasinya, dan satu writer tunggal menulis ulang shard dataset secara in-place.

## Apa yang dihasilkan oleh pipeline

Setiap modul mengeluarkan beberapa jenis anotasi ("gaya"), yang diarahkan ke salah satu dari dua kolom bahasa:

| Gaya / atom                                | Kolom                | Modul          |
| ------------------------------------------- | --------------------- | --------------- |
| `subtask` (gaya Pi0.7 "bagaimana, bukan apa")     | `language_persistent` | `plan`          |
| `plan` (awal + penyegaran saat interupsi)  | `language_persistent` | `plan`          |
| `memory` (kompresi gaya MEM)            | `language_persistent` | `plan`          |
| `task_aug` (parafrasa dari tugas)        | `language_persistent` | `plan`          |
| `interjection`                              | `language_events`     | `interjections` |
| atom tool-call ucapan (`style=null`, `say`) | `language_events`     | `interjections` |
| `vqa` (pasangan user / assistant)               | `language_events`     | `vqa`           |

### Cara subtask dibuat

Modul `plan` tidak meminta subtask dari VLM dalam satu langkah. Sebaliknya, modul ini menggunakan alur **describe → segment** dua langkah:

1. **Describe** — VLM hanya mendeskripsikan apa yang benar-benar terlihat di kamera yang dipilih (tidak menebak-nebak tentang tugasnya).
2. **Segment** — deskripsi tersebut dimasukkan kembali, dan VLM membagi episode menjadi subtask atomik yang berurutan.

Kedua pass melihat episode sebagai **lembar kontak bertanda waktu** — frame yang disampel pada `frames_per_second` (0,5s secara default) dan dikemas ke dalam grid JPEG dengan waktu setiap frame dicetak di sudutnya, sehingga VLM dapat mengutip waktu batas yang tepat secara langsung. Hal ini jauh lebih hemat dalam token visual dibandingkan satu gambar per frame, sehingga sampling dapat tetap padat; episode yang lebih panjang dari `max_frames_per_prompt` dipecah menjadi jendela pada kepadatan yang sama dan digabungkan. Kedua prompt juga membawa definisi **batas peristiwa** yang kausal (peristiwa baru dimulai ketika suatu objek dipegang / dilepaskan / mencapai lokasi baru / penutup berubah status / isi bergerak) untuk memperjelas di mana pemotongan dilakukan.

Secara opsional, jalur ketiga **seeded-relabel** (`--plan.subtask_seeded_relabel`) meninjau ulang setiap span dengan contact sheet segmen sebelumnya/saat ini/berikutnya dan memperbaiki label secara minimal, menggunakan label pertama sebagai prior — jalur ini menjaga batas tetap tetap dan hanya mempertajam perumusan, dengan biaya satu panggilan tambahan per subtask.

Span yang dihasilkan kemudian dirangkai menjadi cakupan episode penuh tanpa celah, sehingga **setiap frame memiliki tepat satu subtask aktif**. Lihat [Running on Hugging Face Jobs](#running-on-hugging-face-jobs) untuk pengaturan produksi (kamera tunggal, contact sheet bertanda waktu, generasi subtask dengan jendela otomatis).

### Alat

Penulis **tidak** menambahkan kolom `tools` ke parquet. Katalog alat berada di `meta/info.json["tools"]` (lihat [Tools](./tools)). Setelah setiap eksekusi, pipeline memastikan skema `say` kanonik ada dalam daftar tersebut, sambil mempertahankan alat apa pun yang Anda deklarasikan sebelumnya.

Ingin menambahkan alat Anda sendiri? Edit `meta/info.json["tools"]` secara langsung — pipeline akan mempertahankan apa pun yang sudah ada. Hal ini membuat alat tersebut terlihat oleh chat template, sehingga model dapat belajar untuk _menghasilkan_ panggilan. Lapisan runtime yang benar-benar _mengeksekusi_ panggilan yang dihasilkan (protokol `Tool` / `TOOL_REGISTRY` di bawah `src/lerobot/tools/`) belum diimplementasikan — dokumen [Tools](./tools) menandai bagian-bagian tersebut sebagai belum diimplementasikan.

## Menjalankan di Hugging Face Jobs

Anotasi dataset nyata membutuhkan GPU yang cukup besar untuk menjalankan VLM, sehingga `lerobot-annotate` dapat mengirim dirinya ke [Hugging Face Jobs](https://huggingface.co/docs/hub/en/jobs) — sama seperti `lerobot-train`. Tambahkan `--job.target=<flavor>` ke perintah persis yang akan Anda jalankan secara lokal, dan ia akan berjalan di perangkat keras tersebut:

```bash
hf auth login   # once

uv run lerobot-annotate \
    --repo_id=user/my_dataset \
    --new_repo_id=user/my_dataset_annotated \
    --push_to_hub=true \
    --vlm.model_id=Qwen/Qwen3.6-27B \
    --vlm.num_gpus=1 \
    --vlm.serve_command="vllm serve Qwen/Qwen3.6-27B --tensor-parallel-size 1 \
        --max-model-len 32768 --gpu-memory-utilization 0.8 \
        --uvicorn-log-level warning --port {port}" \
    --vlm.serve_ready_timeout_s=1800 \
    --vlm.chat_template_kwargs='{"enable_thinking": false}' \
    --job.target=h200
```

Itu mengirimkan tugas `h200` GPU tunggal yang:

1. dimulai dari image `vllm/vllm-openai` dan menginstal `lerobot` di atasnya,
2. menjalankan satu server vLLM per GPU dan mengendalikannya melalui API yang kompatibel dengan OpenAI,
3. menjalankan modul `plan` / `interjections` / `vqa` di seluruh dataset,
4. dengan `--push_to_hub=true`, mengunggah hasil ke `--new_repo_id` (atau kembali ke `--repo_id` di tempatnya jika Anda membiarkannya tidak disetel).

Perintah ini mengalirkan log pekerjaan; `Ctrl-C` memisahkan koneksi tanpa membatalkannya. Daftarkan flavor yang tersedia beserta harganya dengan `hf jobs hardware`.

Qwen3.6 dirilis dengan fitur berpikir yang diaktifkan, yang menghabiskan anggaran token yang dibutuhkan anotator untuk jawaban JSON-nya — `--vlm.chat_template_kwargs='{"enable_thinking": false}'` menonaktifkannya. Tanpa `--push_to_hub=true`, dataset yang telah dianotasi akan dibuang saat pod keluar.

### Opsi pekerjaan

| Bendera                | Default                   | Fungsinya                                                                    |
| ------------------- | ------------------------- | ------------------------------------------------------------------------------- |
| `--job.target`      | `local`                   | Flavor HF Jobs untuk dijalankan (misalnya `h200`, `h200x4`). Dikurangi/`local` berjalan di sini.    |
| `--job.image`       | `vllm/vllm-openai:latest` | Gambar runtime untuk pod.                                                      |
| `--job.timeout`     | `2h`                      | Batas waktu nyata. Naikkan untuk dataset besar.                                    |
| `--job.detach`      | `false`                   | Kirim dan keluar alih-alih streaming log.                                      |
| `--job.lerobot_ref` | `main`                    | Git ref dari lerobot yang diinstal pada pod — arahkan ke cabang untuk menguji perubahan. |
| `--job.tags`        | `[]`                      | Tag tambahan pada job dan pada dataset apa pun yang didorongnya (`lerobot` selalu ditambahkan). |

Untuk dataset yang lebih besar, skala ke `h200x4` dan naikkan `--vlm.parallel_servers` / `--vlm.num_gpus` agar sesuai, serta berikan ruang lebih untuk pekerjaan dengan misalnya `--job.timeout=8h`.

Eksekusi jarak jauh memerlukan `--repo_id` (pod menarik dataset dari Hub; `--root` hanya menamai direktori yang ada di mesin Anda). Dataset yang hanya ada di cache lokal Anda akan didorong terlebih dahulu ke repo **private**.

## Opsi utama

Ini adalah flag yang paling sering Anda gunakan. Jalankan `lerobot-annotate --help` untuk opsi lainnya; nilai defaultnya disesuaikan untuk episode manipulasi yang singkat.

### Dataset masuk / keluar

| Bendera              | Default | Fungsinya                                                            |
| ----------------- | ------- | ----------------------------------------------------------------------- |
| `--repo_id`       | —       | Dataset Hub untuk anotasi (diunduh jika `--root` tidak disetel).                 |
| `--root`          | —       | Anotasi direktori dataset lokal sebagai gantinya.                             |
| `--new_repo_id`   | —       | Dorong hasil ke repo baru (membiarkan repo sumber tetap utuh).       |
| `--push_to_hub`   | `false` | Unggah setelah anotasi (ke `--new_repo_id`, jika tidak kembali ke `--repo_id`). |
| `--only_episodes` | semua     | Anotasi hanya indeks episode ini (berguna untuk uji coba).             |
| `--seed`          | `1729`  | Menetapkan benih untuk RNG yang memilih timestamp interupsi + jenis pertanyaan VQA.  |

### Modul mana yang dijalankan

Setiap modul aktif secara default dan dapat diaktifkan/dinonaktifkan secara independen (atur ke `false` untuk melewatinya, misalnya untuk mengiterasi satu modul pada satu waktu):

| Bendera                      | Default | Matikan                           |
| ------------------------- | ------- | ----------------------------------- |
| `--plan.enabled`          | `true`  | subtasks + rencana + memori + task_aug |
| `--interjections.enabled` | `true`  | interjeksi + atom ucapan        |
| `--vqa.enabled`           | `true`  | pasangan VQA                       |

### VLM (`--vlm.*`)

| Bendera                       | Default            | Fungsinya                                                                         |
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------ |
| `--vlm.model_id`           | `Qwen/Qwen3.6-27B` | Model yang akan disajikan dan prompt.                                                       |
| `--vlm.camera_key`         | pertama `images.*`   | Kamera mana yang menjadi dasar setiap prompt.                                            |
| `--vlm.serve_command`      | auto               | Perintah `vllm serve …` yang tepat (atur ukuran TP, memori GPU, `--max-model-len` di sini).  |
| `--vlm.parallel_servers`   | `1`                | Server independen untuk perutean round-robin (satu per GPU).                           |
| `--vlm.num_gpus`           | `0`                | GPU per server (`0` = satu per server).                                                    |
| `--vlm.client_concurrency` | `16`               | Permintaan in-flight di semua server.                                               |
| `--vlm.max_new_tokens`     | `512`              | Batas generasi per panggilan.                                                             |
| `--vlm.temperature`        | `0.2`              | Suhu sampling.                                                                |
| `--vlm.reasoning_effort`   | `null`             | Petunjuk anggaran berpikir (`low`/`medium`/`high`) diteruskan ke server yang kompatibel dengan OpenAI. |

### Subtugas / rencana / memori (`--plan.*`)

| Bendera                            | Default    | Fungsinya                                                                                                                 |
| ------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--plan.frames_per_second`      | `2.0`      | Laju pengambilan sampel frame untuk contact sheets (`2.0` = satu frame setiap 0,5 detik).                                                   |
| `--plan.max_frames_per_prompt`  | `60`       | Anggaran frame per panggilan VLM. Episode yang sampling-nya melebihi ini akan otomatis di-window pada kepadatan yang sama, kemudian di-stitch.        |
| `--plan.contact_sheet_columns`  | `5`        | Kolom per grid contact-sheet (`contact_sheet_frames_per_sheet` ubin, baris waktu utama).                                     |
| `--plan.plan_max_steps`         | `8`        | Batas atas subtask per episode.                                                                                         |
| `--plan.subtask_describe_first` | `true`     | Jalankan jalur grounding describe→segment (kualitas subtask terbaik; +1 panggilan/episode).                                             |
| `--plan.subtask_seeded_relabel` | `false`    | Lewatan kedua: beri label ulang setiap subtask dari lembar kontak prev/current/next-nya, diinisiasi dengan label pertama (+1 panggilan/subtask). |
| `--plan.subtask_relabel_frames` | `5`        | Frame yang disampel secara seragam per lembar segmen pada jalur relabel (hanya digunakan ketika `subtask_seeded_relabel=true`).               |
| `--plan.emit_plan`              | `true`     | Keluarkan baris `plan` bernomor (`false` = hanya subtask + memori).                                                            |
| `--plan.emit_memory`            | `true`     | Keluarkan baris `memory` (`false` = hanya subtask + rencana); simetris dengan `emit_plan`.                                           |
| `--plan.n_task_rephrasings`     | `10`       | Berapa banyak rephrasing `task_aug` yang akan dikeluarkan (`0` untuk menonaktifkan).                                                                      |
| `--plan.derive_task_from_video` | `if_short` | Gunakan tugas dataset apa adanya (`off`), hanya saat hilang/pendek (`if_short`), atau selalu turunkan ulang dari video (`always`).    |

### Interjeksi + VQA

| Bendera                                            | Default | Fungsinya                                               |
| ----------------------------------------------- | ------- | ---------------------------------------------------------- |
| `--interjections.max_interjections_per_episode` | `3`     | Batas pasangan interupsi/ucapan per episode.              |
| `--vqa.vqa_emission_hz`                         | `1.0`   | Seberapa sering pasangan VQA dipancarkan.                           |
| `--vqa.restrict_to_default_camera`              | `false` | Ground VQA hanya pada `--vlm.camera_key` (jika tidak, semua kamera). |
| `--vqa.coordinate_scale`                        | `1000`  | Grid bbox/keypoint VLM, disimpan sebagai [0, 1] (`none` = px).    |
| `--executor.episode_parallelism`                | `16`    | Episodes yang diproses secara bersamaan dalam setiap fase.         |

## Kontribusi modul baru

Pipeline ini dirancang untuk berkembang, dan **kontribusi sangat diterima** — mulai dari modul baru (misalnya, jejak lintasan atau affordances), template prompt baru, alur grounding yang lebih cerdas, hingga perbaikan kualitas pada modul `plan` / `interjections` / `vqa` yang sudah ada.

Setiap modul berada di bawah `src/lerobot/annotations/steerable_pipeline/modules/`, berbagi klien VLM dan cache keyframe, menulis output mentahnya ke pohon staging, dan terhubung ke executor sebagai fase tersendiri. Punya ide? Buka issue atau PR di [repo](https://github.com/huggingface/lerobot).

## Bagaimana resep mengonsumsi output

Anotasi dimaksudkan untuk dibaca oleh resep (lihat [Kolom Bahasa dan Resep](./language_and_recipes)). Biasanya:

- cabang low-level / high-level / memory-update membaca `subtask` / `plan` / `memory` dari `language_persistent`.
- cabang interupsi-respons membaca peristiwa `interjection` beserta atom ucapan yang berpasangan (digabungkan menjadi satu giliran asisten melalui `tool_calls_from`) dan pembaruan `plan` yang sesuai pada timestamp yang sama.
- cabang VQA membaca pasangan `(vqa, user)` dan `(vqa, assistant)` dari `language_events`.

## Mengapa state dan events dipisahkan

Dua gagasan membentuk desainnya:

1. **State persisten vs. event eksak.** Baris persisten (`subtask`, `plan`, `memory`) berlaku untuk seluruh episode dan menjawab "apa yang benar saat ini?". Baris event (`interjection`, `vqa`, ucapan) hanya muncul pada satu frame yang timestamp-nya cocok. Timestamp disalin langsung dari sumber parquet — tidak pernah dihitung ulang dalam floating point.
2. **Satu kali lintasan VLM.** Ketiga modul berbagi satu klien VLM (klien yang kompatibel dengan OpenAI yang berkomunikasi dengan server vLLM dari job tersebut), sehingga Anda hanya membayar satu kali pemuatan model per dataset, bukan tiga kali.

## Menjalankan ulang satu modul

Setiap modul menaruh output mentahnya ke `<root>/.annotate_staging/episode_{N:06d}/<module>.jsonl`. Hal ini membuat iterasi prompt menjadi murah: menjalankan ulang satu modul hanya menimpa JSONL miliknya sendiri, lalu writer menyusun ulang parquet final. Nonaktifkan modul yang tidak Anda inginkan dengan `--plan.enabled=false` (dan juga `--interjections.enabled` / `--vqa.enabled`) untuk menguji satu per satu.

## Yang diperiksa oleh validator

Sebelum writer dijalankan, `StagingValidator` memastikan:

- setiap baris event mendarat tepat pada timestamp frame yang sebenarnya;
- tidak ada pasangan ucapan / interjeksi yang tersisa tanpa pasangan;
- `plan` diperbarui pada setiap timestamp interupsi;
- baris `memory` jatuh pada batas subtask (peringatan, bukan kesalahan);
- setiap `content` asisten VQA adalah JSON valid dalam salah satu bentuk bbox / keypoint / count / attribute / spatial;
- setiap baris masuk ke kolom yang dipilih oleh `column_for_style(style)`.

Setiap kesalahan akan menghentikan writer. Lewati `--skip_validation=true` untuk menimpa saat debugging.

## Dari mana ide setiap modul berasal

- **`plan` — subtask.** Hi Robot ([Shi 2025](https://arxiv.org/abs/2502.19417)) untuk granularitas atom ("ambil satu helai selada", "letakkan mangkuk ke kotak"); Pi0.7 ([Physical Intelligence 2025](https://pi.website/pi07)) untuk detail "bagaimana, bukan apa".
- **`plan` — memori.** MEM ([Torne 2026](https://arxiv.org/abs/2603.03596)): simpan hanya informasi relevan yang minimal — pertahankan hasil, buang atribut spesifik.
- **`interjections`.** Taksonomi skenario Hi Robot: tugas negatif, koreksi situasional, kendala spesifik, preferensi. Bicara adalah atom yang hanya berupa pemanggilan alat (`tool_calls=[{type:function, function:{name:"say", arguments:{text:...}}}]`).
- **`vqa`.** ECoT ([Zawalski 2024](https://arxiv.org/abs/2407.08693)) untuk fitur yang berakar pada data (bounding box `[x_min, y_min, x_max, y_max]` dan keypoints, disimpan sebagai fraksi gambar [0, 1]) dan Steerable VLA Policies ([Zhao 2025](https://arxiv.org/abs/2509.07626)) untuk grounding multi-abstraksi. Pi0.7 juga mengaitkan jawaban di berbagai tingkat abstraksi.

Saat meningkatkan modul, sesuaikan template prompt-nya di `src/lerobot/annotations/steerable_pipeline/prompts/` alih-alih menulis ulang dari awal.

## Kira-kira berapa biayanya

Per episode, pipeline melakukan sekitar `plan_max_steps` panggilan plan, `max_interjections_per_episode` panggilan interjection, dan `vqa_emission_hz × episode_seconds` panggilan VQA. Dengan nilai default (8 subtask, 3 interjection, 1 Hz × 3 pasangan) pada episode 30 detik, jumlahnya sekitar 50 panggilan VLM.

Penyimpanan tetap kecil: `language_persistent` maksimal puluhan KB per episode (parquet melakukan dictionary-encoding pada satu entri yang berulang di seluruh frame), dan `language_events` kosong di sebagian besar frame — ukurannya berskala dengan jumlah emisi, bukan `num_frames × num_emissions`.

