LeRobot documentation
Pipeline de Anotação
Pipeline de Anotação
lerobot-annotate monitora o vídeo de cada episódio com um modelo de visão-linguagem (VLM) e grava anotações em linguagem natural de volta no seu dataset. Ele preenche as duas colunas de linguagem da página Language Columns and Recipes — language_persistent e language_events — diretamente em data/chunk-*/file-*.parquet.
Em resumo: aponte-o para um dataset do LeRobot, e ele adiciona subtarefas, planos, memória, interjeições, fala e Q&A visual sobre os quais uma política pode ser treinada.
Como tudo se encaixa
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)Três módulos (plan, interjections, vqa) se comunicam com um único VLM compartilhado. Cada módulo grava sua saída em disco, um validador a verifica e um único escritor reescreve os shards do dataset no local.
O que o pipeline produz
Cada módulo emite alguns tipos de anotação (“estilos”), roteados para uma das duas colunas de linguagem:
| Estilo / átomo | Coluna | Módulo |
|---|---|---|
subtask (estilo Pi0.7, “como, não o quê”) | language_persistent | plan |
plan (inicial + atualização em interjeição) | language_persistent | plan |
memory (compressão no estilo MEM) | language_persistent | plan |
task_aug (reformulações da tarefa) | language_persistent | plan |
interjection | language_events | interjections |
áudio de chamada de ferramenta (style=null, say) | language_events | interjections |
vqa (par de usuário / assistente) | language_events | vqa |
Como as subtarefas são geradas
O módulo plan não solicita ao VLM os subtarefas de uma só vez. Em vez disso,
ele usa um fluxo de duas etapas descrever → segmentar:
- Descrever — o VLM narra apenas o que ele realmente vê na câmera escolhida (sem suposições sobre a tarefa).
- Segment — essa descrição é reenviada e o VLM divide o episódio em subtarefas atômicas consecutivas.
Ambas as passadas enxergam o episódio como folhas de contato com carimbo de tempo — quadros amostrados a frames_per_second (0,5s por padrão) e empacotados em grades JPEG com o tempo de cada quadro gravado em seu canto, para que o VLM cite os tempos exatos das fronteiras diretamente. Isso é muito mais barato em tokens de visão do que uma imagem por quadro, então a amostragem pode permanecer densa; episódios mais longos que max_frames_per_prompt são divididos em janelas na mesma densidade e mesclados. Ambos os prompts também carregam uma definição causal de fronteira de evento (um novo evento começa quando um objeto é segurado / é solto / atinge um novo local / uma tampa muda de estado / o conteúdo se move) para afiar onde os cortes são feitos.
Opcionalmente, uma terceira passagem de seeded-relabel (--plan.subtask_seeded_relabel)
revisa cada trecho com as folhas de contato dos segmentos anterior/atual/próximo e
corrige o rótulo de forma mínima, usando o primeiro rótulo como prior — ela mantém
as fronteiras fixas e apenas afina a redação, ao custo de uma chamada extra por
subtarefa.
Os spans resultantes são então unidos em uma cobertura completa do episódio, sem lacunas, de modo que cada quadro tenha exatamente um subtarefa ativa. Consulte Running on Hugging Face Jobs para as configurações de produção (câmera única, contatos com carimbo de data e hora, geração de subtarefas com janelas automáticas).
Ferramentas
The writer does not add a tools column to the parquet. The tool
catalog lives in meta/info.json["tools"] instead (see Tools).
After every run, the pipeline makes sure the canonical say schema is in
that list, keeping any tools you declared beforehand.
Quer adicionar sua própria ferramenta? Edite meta/info.json["tools"] diretamente — o
pipeline preserva o que já estiver lá. Isso torna a ferramenta visível
para o chat template, permitindo que o modelo aprenda a gerar a chamada. A
camada de runtime que realmente executa uma chamada gerada (o protocolo Tool / TOOL_REGISTRY em src/lerobot/tools/) ainda não está
implementada — o documento Tools marca essas partes como não implementadas.
Executando em Hugging Face Jobs
Anotar um dataset real exige uma GPU grande o suficiente para servir o VLM, então o lerobot-annotate pode se despachar para o Hugging Face Jobs — da mesma forma que o lerobot-train. Adicione --job.target=<flavor> ao comando exato que você executaria localmente e ele será executado naquele hardware:
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=h200Isso envia um trabalho de GPU única h200 que:
- começa a partir da imagem
vllm/vllm-openaie instala olerobotpor cima, - inicia um servidor vLLM por GPU e o controla por meio da API compatível com a OpenAI,
- executa os módulos
plan/interjections/vqaem todo o conjunto de dados, - com
--push_to_hub=true, faz o upload do resultado para--new_repo_id(ou de volta para--repo_idno local, se você deixar isso não definido).
O comando transmite os logs do job; Ctrl-C desconecta sem cancelá-lo. Liste os sabores disponíveis e seus preços com hf jobs hardware.
O Qwen3.6 é distribuído com o modo de raciocínio ativado, o que consome o orçamento de tokens de que o anotador precisa para sua resposta em JSON —
--vlm.chat_template_kwargs='{"enable_thinking": false}'desativa esse modo. Sem--push_to_hub=true, o conjunto de dados anotado é descartado quando o pod é encerrado.
Opções de trabalho
| Flag | Padrão | O que ele faz |
|---|---|---|
--job.target | local | Flavor de HF Jobs para executar (por exemplo, h200, h200x4). Omitido/local executa aqui. |
--job.image | vllm/vllm-openai:latest | Imagem de runtime para o pod. |
--job.timeout | 2h | Limite de tempo real. Aumente-o para conjuntos de dados grandes. |
--job.detach | false | Envie e saia em vez de transmitir logs. |
--job.lerobot_ref | main | Ref do Git do lerobot instalado no pod — aponte para um branch para testar alterações. |
--job.tags | [] | Tags extras no job e em qualquer dataset que ele envia (lerobot é sempre adicionado). |
Para um conjunto de dados maior, escale para h200x4 e aumente --vlm.parallel_servers / --vlm.num_gpus de forma correspondente, e dê ao trabalho mais folga com, por exemplo, --job.timeout=8h.
Execuções remotas exigem --repo_id (o pod busca o dataset do Hub; --root nomeia um diretório que existe apenas na sua máquina). Um dataset que existe
apenas no seu cache local é enviado primeiro para um repositório privado.
Opções principais
Estas são as flags que você usará com mais frequência. Execute lerobot-annotate --help para ver o restante; os valores padrão são ajustados para
episódios curtos de manipulação.
Dataset de entrada/saída
| Flag | Padrão | O que ele faz |
|---|---|---|
--repo_id | — | Conjunto de dados do Hub para anotar (baixado se --root não estiver definido). |
--root | — | Anote um diretório de dataset local em vez disso. |
--new_repo_id | — | Envie o resultado para um novo repositório (mantém o repositório de origem intacto). |
--push_to_hub | false | Envie após a anotação (para --new_repo_id, caso contrário, de volta para --repo_id). |
--only_episodes | todos | Anote apenas esses índices de episódios (útil para uma execução de teste). |
--seed | 1729 | Inicializa os RNGs que selecionam os timestamps das interjeições + os tipos de perguntas de VQA. |
Quais módulos são executados
Cada módulo está ativado por padrão e pode ser alternado independentemente (defina como false para ignorá-lo, por exemplo, para iterar em um módulo de cada vez):
| Flag | Padrão | Desativa |
|---|---|---|
--plan.enabled | true | subtasks + plan + memory + task_aug |
--interjections.enabled | true | interjeições + átomos de fala |
--vqa.enabled | true | os pares de VQA |
O VLM ( --vlm.* )
| Flag | Padrão | O que ele faz |
|---|---|---|
--vlm.model_id | Qwen/Qwen3.6-27B | O modelo a ser servido e o prompt. |
--vlm.camera_key | primeiro images.* | Em qual câmera cada prompt é baseado. |
--vlm.serve_command | auto | O comando exato vllm serve … (defina o tamanho do TP, a memória da GPU e o --max-model-len aqui). |
--vlm.parallel_servers | 1 | Servidores independentes para roteamento round-robin (um por GPU). |
--vlm.num_gpus | 0 | GPUs por servidor (0 = uma em cada). |
--vlm.client_concurrency | 16 | Solicitações em andamento em todos os servidores. |
--vlm.max_new_tokens | 512 | Limite de geração por chamada. |
--vlm.temperature | 0.2 | Temperatura de amostragem. |
--vlm.reasoning_effort | null | Dica de orçamento de raciocínio (low/medium/high) encaminhada para servidores compatíveis com a OpenAI. |
Subtarefas / plano / memória ( --plan.* )
| Flag | Padrão | O que ele faz |
|---|---|---|
--plan.frames_per_second | 2.0 | Taxa de amostragem de quadros para as contact sheets (2.0 = um quadro a cada 0,5s). |
--plan.max_frames_per_prompt | 60 | Orçamento de quadros por chamada de VLM. Episódios cuja amostragem excede esse valor são automaticamente enjanelados na mesma densidade e, em seguida, costurados. |
--plan.contact_sheet_columns | 5 | Colunas por grade de contact sheet (contact_sheet_frames_per_sheet tiles, linha do tempo em ordem majoritária). |
--plan.plan_max_steps | 8 | Limite superior de subtarefas por episódio. |
--plan.subtask_describe_first | true | Execute a passagem de grounding describe→segment (melhor qualidade de subtarefa; +1 chamada/episódio). |
--plan.subtask_seeded_relabel | false | Segunda passada: reetiquete cada subtarefa a partir de suas folhas de contato anterior/atual/próxima, iniciando com o primeiro rótulo (+1 chamada/subtarefa). |
--plan.subtask_relabel_frames | 5 | Frames amostrados uniformemente por folha de segmento na passagem de reetiquetagem (usado apenas quando subtask_seeded_relabel=true). |
--plan.emit_plan | true | Emita as linhas numeradas de plan (false = apenas subtarefas + memória). |
--plan.emit_memory | true | Emita as linhas de memory (false = apenas subtarefas + plano); simétrico a emit_plan. |
--plan.n_task_rephrasings | 10 | Quantas reformulações de task_aug emitir (0 desativa). |
--plan.derive_task_from_video | if_short | Use a tarefa do dataset como está (off), apenas quando estiver ausente/curta (if_short) ou sempre rederive do vídeo (always). |
Interjeições + VQA
| Flag | Padrão | O que ele faz |
|---|---|---|
--interjections.max_interjections_per_episode | 3 | Limite de pares de interjeição/fala por episódio. |
--vqa.vqa_emission_hz | 1.0 | Com que frequência os pares VQA são emitidos. |
--vqa.restrict_to_default_camera | false | Baseie o VQA apenas em --vlm.camera_key (caso contrário, todas as câmeras). |
--vqa.coordinate_scale | 1000 | Grade de bbox/keypoint do VLM, armazenada como [0, 1] (none = px). |
--executor.episode_parallelism | 16 | Episódios processados simultaneamente dentro de cada fase. |
Contribuindo com novos módulos
O pipeline foi projetado para crescer, e contribuições são muito bem-vindas —
um módulo totalmente novo (por exemplo, rastreamentos de trajetória ou affordances), um novo
template de prompt, um fluxo de grounding mais inteligente ou correções de qualidade nos módulos existentes plan / interjections / vqa.
Cada módulo fica em src/lerobot/annotations/steerable_pipeline/modules/, compartilha o cliente VLM e o cache de keyframes, grava sua saída bruta na árvore de staging e se conecta ao executor como sua própria fase. Tem uma ideia? Abra uma issue ou PR em o repositório.
Como as receitas consomem a saída
As anotações destinam-se a serem lidas por receitas (veja Colunas de idioma e receitas). Tipicamente:
- Os ramos de baixo nível / alto nível / atualização de memória leem
subtask/plan/memorydelanguage_persistent. - um ramo de interjeição-resposta lê eventos de
interjectione o átomo de fala correspondente (mesclados em um único turno do assistente viatool_calls_from) e a atualização correspondente deplanno mesmo timestamp. - Um ramo de VQA lê os pares
(vqa, user)e(vqa, assistant)delanguage_events.
Por que estado e eventos são separados
Duas ideias moldam o design:
- Estado persistente vs. eventos exatos. Linhas persistentes (
subtask,plan,memory) se aplicam a todo o episódio e respondem à pergunta “o que é verdade agora?”. Linhas de eventos (interjection,vqa, fala) aparecem apenas no frame cujo timestamp corresponde. Os timestamps são copiados diretamente do parquet de origem — nunca recalculados em ponto flutuante. - Uma única passagem do VLM. Os três módulos compartilham um único cliente de VLM (o cliente compatível com a OpenAI que se comunica com o servidor vLLM do job), de modo que você paga por um único carregamento de modelo por dataset, e não por três.
Executando novamente um único módulo
Cada módulo grava sua saída bruta em <root>/.annotate_staging/episode_{N:06d}/<module>.jsonl. Isso torna a iteração de prompts barata: reexecutar um módulo sobrescreve apenas o JSONL dele, e em seguida o writer recompor o parquet final. Desative os módulos que você não deseja com --plan.enabled=false (e da mesma forma --interjections.enabled / --vqa.enabled) para testá-los um por um.
O que o validador verifica
Antes de o writer ser executado, StagingValidator confirma:
- cada linha de evento corresponde exatamente a um timestamp real de quadro;
- não há pares de fala / interjeição que fiquem órfãos;
plané atualizado a cada timestamp de interjeição;- linhas de
memorycaem nas fronteiras de subtarefas (um aviso, não um erro); - cada conteúdo
contentdo assistente VQA é um JSON válido em um dos formatos bbox / keypoint / count / attribute / spatial; - cada linha vai para a coluna escolhida por
column_for_style(style).
Qualquer erro interrompe o escritor. Passe --skip_validation=true para ignorar durante a depuração.
De onde vêm as ideias de cada módulo
plan— subtarefas. Hi Robot (Shi 2025) para granularidade atômica (“pegar uma folha de alface”, “colocar a tigela na caixa”); Pi0.7 (Physical Intelligence 2025) para detalhes de “como, não o quê”.plan— memória. MEM (Torne 2026): mantenha apenas as informações mínimas relevantes — preserve os resultados, descarte atributos específicos.interjections. Taxonomia de cenários do Hi Robot: tarefa negativa, correção situada, restrição específica, preferência. A fala é um átomo exclusivo de chamada de ferramenta (tool_calls=[{type:function, function:{name:"say", arguments:{text:...}}}]).vqa. ECoT (Zawalski 2024) para recursos fundamentados (bounding boxes[x_min, y_min, x_max, y_max]e keypoints, armazenados como frações de imagem em [0, 1]) e Steerable VLA Policies (Zhao 2025) para fundamentação em múltiplos níveis de abstração. O Pi0.7 também fundamenta respostas em diferentes níveis de abstração.
Ao melhorar um módulo, ajuste seu modelo de prompt em src/lerobot/annotations/steerable_pipeline/prompts/ em vez de reescrevê-lo do zero.
Aproximadamente quanto custa
Por episódio, o pipeline faz cerca de plan_max_steps chamadas de plano, max_interjections_per_episode chamadas de interjeição e vqa_emission_hz × episode_seconds chamadas de VQA. Com os padrões (8 subtarefas, 3 interjeições, 1 Hz × 3 pares) em um episódio de 30 segundos, isso resulta em ~50 chamadas de VLM.
O armazenamento permanece pequeno: language_persistent tem no máximo algumas dezenas de KB por episódio (o parquet codifica por dicionário a única entrada que se repete entre os frames) e language_events está vazio na maioria dos frames — seu tamanho escala com o número de emissões, não com num_frames × num_emissions.