LeRobot documentation

Pipeline d'annotation

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

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 / atomeColonneModule
subtask (style Pi0.7 « comment, pas quoi »)language_persistentplan
plan (initial + refresh on interjection)language_persistentplan
memory (compression de type MEM)language_persistentplan
task_aug (reformulations de la tâche)language_persistentplan
interjectionlanguage_eventsinterjections
atome d'appel d'outil vocal (style=null, say)language_eventsinterjections
vqa (paire utilisateur / assistant)language_eventsvqa

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 :

  1. 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).
  2. 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=h200

Cela soumet un travail sur un seul GPU h200 qui :

  1. part de l’image vllm/vllm-openai et installe lerobot par-dessus,
  2. démarrant un serveur vLLM par GPU et le pilotant via l'API compatible OpenAI,
  3. exécute les modules plan / interjections / vqa sur le jeu de données,
  4. 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

DrapeauPar défautCe qu'il fait
--job.targetlocalSaveur de HF Jobs sur laquelle exécuter (par ex. h200, h200x4). Omis/local exécute ici.
--job.imagevllm/vllm-openai:latestImage d'exécution pour le pod.
--job.timeout2hLimite de temps réel. Augmentez-la pour les grands jeux de données.
--job.detachfalseSoumettre et quitter au lieu de diffuser les journaux.
--job.lerobot_refmainRé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

DrapeauPar défautCe 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_hubfalseTéléverser après l'annotation (vers --new_repo_id, sinon vers --repo_id).
--only_episodestousAnnoter uniquement ces indices d'épisode (pratique pour un essai).
--seed1729Initialise 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) :

DrapeauPar défautDésactive
--plan.enabledtruesous-tâches + plan + mémoire + task_aug
--interjections.enabledtrueinterjections + atomes de parole
--vqa.enabledtrueles paires VQA

Le VLM ( --vlm.* )

DrapeauPar défautCe qu'il fait
--vlm.model_idQwen/Qwen3.6-27BLe modèle à servir et le prompt.
--vlm.camera_keypremier images.*La caméra sur laquelle chaque invite est basée.
--vlm.serve_commandautoLa commande exacte vllm serve … (définissez la taille TP, la mémoire GPU, --max-model-len ici).
--vlm.parallel_servers1Serveurs indépendants pour le routage round-robin (un par GPU).
--vlm.num_gpus0GPU par serveur (0 = un chacun).
--vlm.client_concurrency16Demandes en cours sur tous les serveurs.
--vlm.max_new_tokens512Limite de génération par appel.
--vlm.temperature0.2Température d'échantillonnage.
--vlm.reasoning_effortnullIndice de budget de réflexion (low/medium/high) transmis aux serveurs compatibles OpenAI.

Sous-tâches / plan / mémoire ( --plan.* )

DrapeauPar défautCe qu'il fait
--plan.frames_per_second2.0Taux d'échantillonnage des images pour les feuilles de contact (2.0 = une image toutes les 0,5 s).
--plan.max_frames_per_prompt60Budget 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_columns5Colonnes par grille de contact (contact_sheet_frames_per_sheet tuiles, lignes temporelles en ordre majeur).
--plan.plan_max_steps8Limite supérieure du nombre de sous-tâches par épisode.
--plan.subtask_describe_firsttrueExécutez le passage de mise en correspondance describe→segment (meilleure qualité de sous-tâche ; +1 appel/épisode).
--plan.subtask_seeded_relabelfalseDeuxiè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_frames5Images échantillonnées uniformément par feuille de segment lors du passage de réétiquetage (utilisé uniquement lorsque subtask_seeded_relabel=true).
--plan.emit_plantrueÉmettre les lignes numérotées de plan (false = sous-tâches + mémoire uniquement).
--plan.emit_memorytrueÉmettre les lignes memory (false = sous-tâches + plan uniquement) ; symétrique à emit_plan.
--plan.n_task_rephrasings10Nombre de reformulations task_aug à émettre (0 pour désactiver).
--plan.derive_task_from_videoif_shortUtilisez 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

DrapeauPar défautCe qu'il fait
--interjections.max_interjections_per_episode3Limite le nombre de paires interjection/parole par épisode.
--vqa.vqa_emission_hz1.0Fréquence d'émission des paires VQA.
--vqa.restrict_to_default_camerafalseFondez la VQA uniquement sur --vlm.camera_key (sinon toutes les caméras).
--vqa.coordinate_scale1000Grille de bbox/keypoint VLM, stockée dans [0, 1] (none = px).
--executor.episode_parallelism16É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 / memory depuis language_persistent.
  • une branche interjection-réponse lit les événements interjection ainsi que l’atome de parole apparié (fusionné en un seul tour assistant via tool_calls_from) et la mise à jour correspondante de plan au même horodatage.
  • Une branche VQA lit les paires (vqa, user) et (vqa, assistant) à partir de language_events.

Pourquoi l'état et les événements sont séparés

Deux idées façonnent la conception :

  1. É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.
  2. 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 ;
  • plan est actualisé à chaque horodatage d'interjection ;
  • Les lignes de memory tombent sur des limites de sous-tâche (un avertissement, pas une erreur) ;
  • chaque contenu content d’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.

Update on GitHub