LeRobot documentation
Pipeline d'annotation
Pipeline d'annotation
lerobot-annotate surveille la vidéo de chaque épisode avec un modèle vision-langage (VLM) et réécrit des annotations en langage naturel dans votre jeu de données. Il remplit les deux colonnes de langage de la page Colonnes de langage et recettes — language_persistent et language_events — directement dans data/chunk-*/file-*.parquet.
En résumé : pointez-le vers un jeu de données LeRobot, et il ajoute des sous-tâches, des plans, une mémoire, des interjections, de la parole et des questions-réponses visuelles sur lesquelles une politique peut être entraînée.
Comment tout s'articule
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)Trois modules (plan, interjections, vqa) communiquent tous avec un VLM partagé. Chaque module enregistre sa sortie sur le disque, un validateur la vérifie, puis un unique rédacteur réécrit les shards du jeu de données sur place.
Ce que le pipeline produit
Chaque module émet plusieurs types d’annotation (« styles »), acheminés vers l’une des deux colonnes de langue :
| Style / atome | Colonne | Module |
|---|---|---|
subtask (style Pi0.7 « comment, pas quoi ») | language_persistent | plan |
plan (initial + refresh on interjection) | language_persistent | plan |
memory (compression de type MEM) | language_persistent | plan |
task_aug (reformulations de la tâche) | language_persistent | plan |
interjection | language_events | interjections |
atome d'appel d'outil vocal (style=null, say) | language_events | interjections |
vqa (paire utilisateur / assistant) | language_events | vqa |
Génération des sous-tâches
Le module plan ne demande pas au VLM de fournir les sous-tâches en une seule fois. Il utilise à la place un flux en deux étapes describe → segment :
- Décrire — le VLM ne décrit que ce qu'il voit réellement dans la caméra choisie (aucune supposition sur la tâche).
- Segment — cette description est renvoyée, et le VLM découpe l'épisode en sous-tâches atomiques consécutives.
Les deux passes perçoivent l’épisode sous forme de contact sheets horodatées — des images échantillonnées à frames_per_second (0,5 s par défaut) et regroupées en grilles JPEG, avec l’horodatage de chaque image inscrit dans son coin, ce qui permet au VLM de citer directement les temps exacts des frontières. Cette approche est bien moins coûteuse en jetons visuels qu’une image par image, ce qui permet de maintenir un échantillonnage dense ; les épisodes plus longs que max_frames_per_prompt sont divisés en fenêtres à la même densité puis fusionnés. Les deux prompts incluent également une définition causale de la frontière d’événement (un nouvel événement commence lorsqu’un objet est saisi / relâché / atteint un nouvel emplacement / un couvercle change d’état / le contenu se déplace) afin de préciser l’emplacement des coupes.
Facultativement, un troisième passage seeded-relabel (--plan.subtask_seeded_relabel)
revisite chaque segment avec ses feuilles de contact des segments précédent/actuel/suivant et
corrige minimalement l'étiquette, en utilisant la première étiquette comme a priori — il conserve
les limites fixes et ne fait que préciser le libellé, au prix d'un appel supplémentaire par
sous-tâche.
Les segments résultants sont ensuite assemblés pour former une couverture complète de l'épisode, sans lacune, de sorte que chaque image possède exactement une sous-tâche active. Consultez Running on Hugging Face Jobs pour les paramètres de production (caméra unique, planches de contact horodatées, génération de sous-tâches par fenêtres automatiques).
Outils
Le writer n’ajoute pas de colonne tools au parquet. Le catalogue d’outils se trouve plutôt dans meta/info.json["tools"] (voir Tools). Après chaque exécution, le pipeline s’assure que le schéma canonique say est présent dans cette liste, en conservant les outils que vous avez déclarés au préalable.
Souhaitez-vous ajouter votre propre outil ? Modifiez directement meta/info.json["tools"] — le pipeline conserve tout ce qui y figure déjà. Cela rend l’outil visible pour le modèle de chat, permettant ainsi au modèle d’apprendre à générer l’appel. La couche d’exécution qui exécute réellement un appel généré (le protocole Tool / TOOL_REGISTRY sous src/lerobot/tools/) n’est pas encore implémentée — la documentation Tools marque ces éléments comme non implémentés.
Exécution sur Hugging Face Jobs
L’annotation d’un jeu de données réel nécessite un GPU suffisamment puissant pour servir le VLM, lerobot-annotate peut donc se déléguer à Hugging Face Jobs — de la même manière que lerobot-train. Ajoutez --job.target=<flavor> à la commande exacte que vous exécuteriez localement et elle s’exécutera sur ce matériel à la place :
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=h200Cela soumet un travail sur un seul GPU h200 qui :
- part de l’image
vllm/vllm-openaiet installelerobotpar-dessus, - démarrant un serveur vLLM par GPU et le pilotant via l'API compatible OpenAI,
- exécute les modules
plan/interjections/vqasur le jeu de données, - avec
--push_to_hub=true, téléverse le résultat dans--new_repo_id(ou dans--repo_idà la place si vous laissez ce dernier non défini).
La commande affiche les journaux du travail en continu ; Ctrl-C se détache sans l’annuler. Listez les saveurs disponibles et leur tarification avec hf jobs hardware.
Qwen3.6 est livré avec la réflexion activée, ce qui consomme le budget de jetons dont l’annotateur a besoin pour sa réponse JSON —
--vlm.chat_template_kwargs='{"enable_thinking": false}'la désactive. Sans--push_to_hub=true, le jeu de données annoté est supprimé lorsque le pod se termine.
Options de tâche
| Drapeau | Par défaut | Ce qu'il fait |
|---|---|---|
--job.target | local | Saveur de HF Jobs sur laquelle exécuter (par ex. h200, h200x4). Omis/local exécute ici. |
--job.image | vllm/vllm-openai:latest | Image d'exécution pour le pod. |
--job.timeout | 2h | Limite de temps réel. Augmentez-la pour les grands jeux de données. |
--job.detach | false | Soumettre et quitter au lieu de diffuser les journaux. |
--job.lerobot_ref | main | Référence Git de lerobot installé sur le pod — pointez-la vers une branche pour tester les modifications. |
--job.tags | [] | Tags supplémentaires sur le job et sur tout jeu de données qu'il pousse (lerobot est toujours ajouté). |
Pour un jeu de données plus volumineux, passez à h200x4 et augmentez --vlm.parallel_servers / --vlm.num_gpus en conséquence, et accordez plus de marge au travail, par exemple avec --job.timeout=8h.
Les exécutions distantes nécessitent --repo_id (le pod récupère le jeu de données depuis le Hub ; --root désigne un répertoire présent uniquement sur votre machine). Un jeu de données qui n’existe que dans votre cache local est d’abord poussé vers un dépôt privé.
Options principales
Voici les options que vous utiliserez le plus souvent. Exécutez lerobot-annotate --help pour tout le reste ; les valeurs par défaut sont optimisées pour les épisodes de manipulation courts.
Entrée / sortie de jeu de données
| Drapeau | Par défaut | Ce qu'il fait |
|---|---|---|
--repo_id | — | Jeu de données du Hub à annoter (téléchargé si --root n'est pas défini). |
--root | — | Annotez plutôt un répertoire de jeu de données local. |
--new_repo_id | — | Poussez le résultat vers un nouveau dépôt (le dépôt source reste inchangé). |
--push_to_hub | false | Téléverser après l'annotation (vers --new_repo_id, sinon vers --repo_id). |
--only_episodes | tous | Annoter uniquement ces indices d'épisode (pratique pour un essai). |
--seed | 1729 | Initialise les générateurs aléatoires qui sélectionnent les horodatages des interjections et les types de questions VQA. |
Quels modules s'exécutent
Chaque module est activé par défaut et peut être activé ou désactivé indépendamment (définissez-le sur false pour l’ignorer, par exemple pour travailler sur un module à la fois) :
| Drapeau | Par défaut | Désactive |
|---|---|---|
--plan.enabled | true | sous-tâches + plan + mémoire + task_aug |
--interjections.enabled | true | interjections + atomes de parole |
--vqa.enabled | true | les paires VQA |
Le VLM ( --vlm.* )
| Drapeau | Par défaut | Ce qu'il fait |
|---|---|---|
--vlm.model_id | Qwen/Qwen3.6-27B | Le modèle à servir et le prompt. |
--vlm.camera_key | premier images.* | La caméra sur laquelle chaque invite est basée. |
--vlm.serve_command | auto | La commande exacte vllm serve … (définissez la taille TP, la mémoire GPU, --max-model-len ici). |
--vlm.parallel_servers | 1 | Serveurs indépendants pour le routage round-robin (un par GPU). |
--vlm.num_gpus | 0 | GPU par serveur (0 = un chacun). |
--vlm.client_concurrency | 16 | Demandes en cours sur tous les serveurs. |
--vlm.max_new_tokens | 512 | Limite de génération par appel. |
--vlm.temperature | 0.2 | Température d'échantillonnage. |
--vlm.reasoning_effort | null | Indice de budget de réflexion (low/medium/high) transmis aux serveurs compatibles OpenAI. |
Sous-tâches / plan / mémoire ( --plan.* )
| Drapeau | Par défaut | Ce qu'il fait |
|---|---|---|
--plan.frames_per_second | 2.0 | Taux d'échantillonnage des images pour les feuilles de contact (2.0 = une image toutes les 0,5 s). |
--plan.max_frames_per_prompt | 60 | Budget de frames par appel VLM. Les épisodes dont l'échantillonnage dépasse cette valeur sont automatiquement fenêtrés à la même densité, puis assemblés. |
--plan.contact_sheet_columns | 5 | Colonnes par grille de contact (contact_sheet_frames_per_sheet tuiles, lignes temporelles en ordre majeur). |
--plan.plan_max_steps | 8 | Limite supérieure du nombre de sous-tâches par épisode. |
--plan.subtask_describe_first | true | Exécutez le passage de mise en correspondance describe→segment (meilleure qualité de sous-tâche ; +1 appel/épisode). |
--plan.subtask_seeded_relabel | false | Deuxième passe : réétiqueter chaque sous-tâche à partir de ses feuilles de contact précédente/actuelle/suivante, initialisées avec l'étiquette première (+1 appel/sous-tâche). |
--plan.subtask_relabel_frames | 5 | Images échantillonnées uniformément par feuille de segment lors du passage de réétiquetage (utilisé uniquement lorsque subtask_seeded_relabel=true). |
--plan.emit_plan | true | Émettre les lignes numérotées de plan (false = sous-tâches + mémoire uniquement). |
--plan.emit_memory | true | Émettre les lignes memory (false = sous-tâches + plan uniquement) ; symétrique à emit_plan. |
--plan.n_task_rephrasings | 10 | Nombre de reformulations task_aug à émettre (0 pour désactiver). |
--plan.derive_task_from_video | if_short | Utilisez la tâche du jeu de données telle quelle (off), uniquement lorsqu’elle est manquante ou trop courte (if_short), ou re-déduisez-la toujours à partir de la vidéo (always). |
Interjections + VQA
| Drapeau | Par défaut | Ce qu'il fait |
|---|---|---|
--interjections.max_interjections_per_episode | 3 | Limite le nombre de paires interjection/parole par épisode. |
--vqa.vqa_emission_hz | 1.0 | Fréquence d'émission des paires VQA. |
--vqa.restrict_to_default_camera | false | Fondez la VQA uniquement sur --vlm.camera_key (sinon toutes les caméras). |
--vqa.coordinate_scale | 1000 | Grille de bbox/keypoint VLM, stockée dans [0, 1] (none = px). |
--executor.episode_parallelism | 16 | Épisodes traités de manière concurrente au sein de chaque phase. |
Contribuer de nouveaux modules
Le pipeline est conçu pour évoluer, et les contributions sont très bienvenues —
un tout nouveau module (par exemple, des traces de trajectoire ou des affordances), un nouveau
modèle de prompt, un flux de grounding plus intelligent, ou des corrections de qualité pour les modules existants plan / interjections / vqa.
Chaque module se trouve sous src/lerobot/annotations/steerable_pipeline/modules/, partage le client VLM et le cache de keyframes, écrit sa sortie brute dans l'arborescence de staging et s'intègre à l'exécuteur en tant que phase distincte. Une idée ? Ouvrez une issue ou un PR sur le dépôt.
Comment les recettes consomment la sortie
Les annotations sont destinées à être lues par les recettes (voir Colonnes de langue et recettes). Typiquement :
- Les branches low-level / high-level / memory-update lisent
subtask/plan/memorydepuislanguage_persistent. - une branche interjection-réponse lit les événements
interjectionainsi que l’atome de parole apparié (fusionné en un seul tour assistant viatool_calls_from) et la mise à jour correspondante deplanau même horodatage. - Une branche VQA lit les paires
(vqa, user)et(vqa, assistant)à partir delanguage_events.
Pourquoi l'état et les événements sont séparés
Deux idées façonnent la conception :
- État persistant et événements exacts. Les lignes persistantes (
subtask,plan,memory) s'appliquent à l'épisode entier et répondent à la question « qu'est-ce qui est vrai maintenant ? ». Les lignes d'événements (interjection,vqa, parole) n'apparaissent que sur le seul frame dont le timestamp correspond. Les timestamps sont copiés tels quels depuis le parquet source — jamais recalculés en virgule flottante. - Un seul passage VLM. Les trois modules partagent un client VLM unique (le client compatible OpenAI qui communique avec le serveur vLLM du job), ce qui signifie que vous payez pour un seul chargement de modèle par jeu de données, et non pour trois.
Réexécution d'un module unique
Chaque module enregistre sa sortie brute dans <root>/.annotate_staging/episode_{N:06d}/<module>.jsonl. Cela rend l’itération sur les prompts peu coûteuse : relancer un module n’écrase que son propre JSONL, puis le writer recompose le parquet final. Désactivez les modules que vous ne souhaitez pas exécuter avec --plan.enabled=false (et de même --interjections.enabled / --vqa.enabled) pour les tester un par un.
Ce que le validateur vérifie
Avant l’exécution du writer, StagingValidator confirme :
- chaque ligne d'événement correspond exactement à un horodatage de frame réel ;
- aucune paire de parole / interjection n'est laissée orpheline ;
planest actualisé à chaque horodatage d'interjection ;- Les lignes de
memorytombent sur des limites de sous-tâche (un avertissement, pas une erreur) ; - chaque contenu
contentd’assistant VQA est un JSON valide dans l’une des formes bbox / keypoint / count / attribute / spatial ; - chaque ligne est envoyée dans la colonne choisie par
column_for_style(style).
Toute erreur interrompt l'écriture. Passez --skip_validation=true pour contourner cette vérification pendant le débogage.
D'où viennent les idées de chaque module
plan— sous-tâches. Hi Robot (Shi 2025) pour la granularité atomique (« prendre une feuille de laitue », « placer le bol dans la boîte ») ; Pi0.7 (Physical Intelligence 2025) pour le détail du « comment, pas du quoi ».plan— mémoire. MEM (Torne 2026) : conservez uniquement les informations minimales pertinentes — préservez les résultats, supprimez les attributs spécifiques.interjections. Taxonomie des scénarios de Hi Robot : tâche négative, correction située, contrainte spécifique, préférence. La parole est un atome réservé aux appels d’outils (tool_calls=[{type:function, function:{name:"say", arguments:{text:...}}}]).vqa. ECoT (Zawalski 2024) pour les fonctionnalités ancrées (boîtes englobantes[x_min, y_min, x_max, y_max]et points clés, stockés sous forme de fractions d'image dans [0, 1]) et les politiques VLA orientables (Zhao 2025) pour l'ancrage multi-abstraction. Pi0.7 ancre également les réponses à travers les niveaux d'abstraction.
Lors de l’amélioration d’un module, ajustez son modèle de prompt dans src/lerobot/annotations/steerable_pipeline/prompts/ plutôt que de le réécrire à partir de zéro.
Environ combien ça coûte
À chaque épisode, le pipeline effectue environ plan_max_steps appels de planification, max_interjections_per_episode appels d’interjection et vqa_emission_hz × episode_seconds appels VQA. Avec les valeurs par défaut (8 sous-tâches, 3 interjections, 1 Hz × 3 paires) sur un épisode de 30 secondes, cela représente environ 50 appels VLM.
L’empreinte disque reste faible : language_persistent occupe au plus quelques dizaines de Ko par épisode (parquet encode par dictionnaire la seule entrée qui se répète d’une image à l’autre), et language_events est vide sur la plupart des images — sa taille dépend du nombre d’émissions, et non de num_frames × num_emissions.