LeRobot documentation

Конвейер аннотации

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Конвейер аннотации

lerobot-annotate анализирует видео каждого эпизода с помощью модели «зрение-язык» (VLM) и записывает аннотации на естественном языке обратно в ваш датасет. Он заполняет два языковых столбца со страницы Language Columns and Recipes — language_persistent и language_events — непосредственно в файлы data/chunk-*/file-*.parquet.

Коротко: укажите ему датасет LeRobot, и он добавит подзадачи, планы, память, вставки, речь и визуальные вопросы и ответы, на которых можно обучить политику.

Как всё это работает

  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)

Три модуля (plan, interjections, vqa) взаимодействуют с одной общей VLM. Каждый модуль записывает свой вывод на диск, валидатор проверяет его, а единый writer перезаписывает шарды датасета на месте.

Что генерирует конвейер

Каждый модуль генерирует несколько типов аннотаций («стилей»), направляемых в один из двух языковых столбцов:

Стиль / атомСтолбецМодуль
subtask (в стиле Pi0.7: «как, а не что»)language_persistentplan
plan (начальный + обновление при вмешательстве)language_persistentplan
memory (сжатие в стиле MEM)language_persistentplan
task_aug (перефразировки задачи)language_persistentplan
interjectionlanguage_eventsinterjections
атом вызова инструмента речи (style=null, say)language_eventsinterjections
vqa (пара «пользователь / ассистент»)language_eventsvqa

Как создаются подзадачи

Модуль plan не запрашивает у VLM подзадачи одним запросом. Вместо этого он использует двухэтапный процесс describe → segment:

  1. Описание — VLM описывает только то, что действительно видит на выбранной камере (без догадок о задаче).
  2. Segment — это описание возвращается обратно, и VLM разбивает эпизод на последовательные атомарные подзадачи.

Оба прохода воспринимают эпизод как контактные листы с временными метками — кадры, отобранные с частотой frames_per_second (по умолчанию 0,5 с) и упакованные в JPEG-сетки, где время каждого кадра вписано в его угол, благодаря чему VLM указывает точные времена границ напрямую. Это значительно дешевле по числу визуальных токенов, чем один кадр на изображение, поэтому выборку можно делать плотной; эпизоды длиннее max_frames_per_prompt разбиваются на окна с той же плотностью и затем объединяются. Оба промпта также содержат причинное определение границы события (новое событие начинается, когда объект берут в руки / отпускают / он достигает нового местоположения / крышка меняет состояние / содержимое перемещается), чтобы точнее определить, где должны быть разрывы.

Необязательно, третий проход seeded-relabel (--plan.subtask_seeded_relabel) повторно рассматривает каждый фрагмент с контактными листами предыдущего/текущего/следующего сегмента и минимально корректирует метку, используя первую метку как априорную — он сохраняет границы фиксированными и лишь уточняет формулировку, ценой одного дополнительного вызова на подзадачу.

Полученные фрагменты затем склеиваются в непрерывное покрытие всего эпизода, так что каждый кадр имеет ровно один активный подзадачу. См. Running on Hugging Face Jobs для настроек продакшена (одна камера, контактные листы с временными метками, автоматическая генерация подзадач с оконным разбиением).

Инструменты

Записывающий модуль не добавляет столбец tools в parquet. Каталог инструментов хранится в meta/info.json["tools"] (см. Tools). После каждого запуска конвейер проверяет, что каноническая схема say присутствует в этом списке, сохраняя при этом все инструменты, объявленные вами ранее.

Хотите добавить собственный инструмент? Отредактируйте meta/info.json["tools"] напрямую — конвейер сохраняет всё, что уже там есть. Это делает инструмент видимым для шаблона чата, чтобы модель могла научиться генерировать вызов. Слой рантайма, который фактически выполняет сгенерированный вызов (протокол Tool / TOOL_REGISTRY в src/lerobot/tools/), пока не реализован — документация Tools помечает эти части как не реализованные.

Запуск на Hugging Face Jobs

Аннотирование реального набора данных требует GPU достаточно большого размера для обслуживания VLM, поэтому lerobot-annotate может быть запущен на Hugging Face Jobs — так же, как и lerobot-train. Добавьте --job.target=<flavor> к той команде, которую вы бы выполнили локально, и она будет запущена на этом оборудовании:

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

Это отправляет задание на один GPU h200, которое:

  1. использует образ vllm/vllm-openai и устанавливает поверх него lerobot,
  2. запускает один сервер vLLM на каждый GPU и управляет им через OpenAI-совместимый API,
  3. запускает модули plan / interjections / vqa на наборе данных,
  4. с --push_to_hub=true загружает результат в --new_repo_id (или обратно в --repo_id на месте, если вы оставите его не заданным).

Команда выводит логи задания в режиме потока; Ctrl-C отключает вывод, не отменяя задание. Список доступных конфигураций и их стоимость можно получить с помощью hf jobs hardware.

Qwen3.6 поставляется с включённым режимом размышлений, который расходует бюджет токенов, необходимый аннотатору для ответа в формате JSON — --vlm.chat_template_kwargs='{"enable_thinking": false}' отключает его. Без --push_to_hub=true размеченный набор данных будет удалён при завершении работы пода.

Варианты заданий

ФлагПо умолчаниюЧто он делает
--job.targetlocalВариант HF Jobs для запуска (например, h200, h200x4). Пропущено/local выполняется здесь.
--job.imagevllm/vllm-openai:latestОбраз среды выполнения для пода.
--job.timeout2hОграничение по фактическому времени. Увеличьте его для больших наборов данных.
--job.detachfalseОтправить и выйти, а не транслировать журналы.
--job.lerobot_refmainGit-ссылка на установленную на поде версию lerobot — укажите ветку для тестирования изменений.
--job.tags[]Дополнительные теги для задания и для любого набора данных, который оно публикует (lerobot добавляется всегда).

Для более крупного набора данных масштабируйте до h200x4 и увеличьте значения --vlm.parallel_servers / --vlm.num_gpus соответственно, а также предоставьте задаче больше запаса, например, с помощью --job.timeout=8h.

Для удалённых запусков требуется --repo_id (под забирает датасет с Hub; --root указывает на каталог, который есть только на вашей машине). Датасет, существующий только в вашем локальном кэше, сначала выгружается в приватный репозиторий.

Ключевые параметры

Это флаги, которые вы будете использовать чаще всего. Запустите lerobot-annotate --help, чтобы увидеть остальные; значения по умолчанию настроены для коротких эпизодов манипуляции.

Данные на входе / выходе

ФлагПо умолчаниюЧто он делает
--repo_id—Набор данных на Hub для аннотирования (будет загружен, если --root не задан).
--root—Вместо этого аннотируйте локальный каталог набора данных.
--new_repo_id—Отправьте результат в новый репозиторий (исходный репозиторий остаётся без изменений).
--push_to_hubfalseЗагрузка после аннотирования (в --new_repo_id, иначе обратно в --repo_id).
--only_episodesвсеАннотируйте только эти индексы эпизодов (удобно для тестового запуска).
--seed1729Инициализирует генераторы случайных чисел, которые выбирают временные метки вставок и типы вопросов VQA.

Какие модули выполняются

Каждый модуль включён по умолчанию и может быть переключён независимо (установите false, чтобы пропустить его, например, чтобы работать с одним модулем за раз):

ФлагПо умолчаниюОтключает
--plan.enabledtruesubtasks + plan + memory + task_aug
--interjections.enabledtrueмеждометия + речевые атомы
--vqa.enabledtrueVQA-пары

VLM ( --vlm.* )

ФлагПо умолчаниюЧто он делает
--vlm.model_idQwen/Qwen3.6-27BМодель для обслуживания и запрос.
--vlm.camera_keyсначала images.*Камера, на которую опирается каждый промпт.
--vlm.serve_commandautoТочная команда vllm serve … (укажите здесь размер TP, память GPU, --max-model-len).
--vlm.parallel_servers1Независимые серверы для маршрутизации по кругу (по одному на каждый GPU).
--vlm.num_gpus0GPU на сервер (0 = по одному).
--vlm.client_concurrency16Запросы в процессе выполнения на всех серверах.
--vlm.max_new_tokens512Лимит генерации на один вызов.
--vlm.temperature0.2Температура выборки.
--vlm.reasoning_effortnullПодсказка thinking-budget (low/medium/high), передаваемая на серверы, совместимые с OpenAI.

Подзадачи / план / память ( --plan.* )

ФлагПо умолчаниюЧто он делает
--plan.frames_per_second2.0Частота выборки кадров для контактных листов (2.0 = один кадр каждые 0,5 с).
--plan.max_frames_per_prompt60Бюджет кадров на вызов VLM. Эпизоды, выборка которых превышает этот лимит, автоматически обрабатываются с окнами той же плотности, а затем склеиваются.
--plan.contact_sheet_columns5Столбцов в сетке контактного листа (contact_sheet_frames_per_sheet тайлов, строки по времени).
--plan.plan_max_steps8Верхняя граница количества подзадач на эпизод.
--plan.subtask_describe_firsttrueВыполните проход describe→segment для привязки (наилучшее качество подзадачи; +1 вызов/эпизод).
--plan.subtask_seeded_relabelfalseВторой проход: переметить каждый подзадачу по её контактным листам prev/current/next, используя начальную метку (+1 вызов/подзадача).
--plan.subtask_relabel_frames5Кадров, равномерно отобранных на лист сегмента в проходе повторной разметки (используется только при subtask_seeded_relabel=true).
--plan.emit_plantrueВыводить нумерованные строки plan (false = только подзадачи и память).
--plan.emit_memorytrueВыводить строки memory (false = только подзадачи и план); симметрично emit_plan.
--plan.n_task_rephrasings10Сколько перефразировок task_aug выводить (0 отключает).
--plan.derive_task_from_videoif_shortИспользуйте задачу датасета как есть (off), только когда она отсутствует/короткая (if_short) или всегда вычисляйте заново из видео (always).

Междометия + VQA

ФлагПо умолчаниюЧто он делает
--interjections.max_interjections_per_episode3Ограничение на количество пар «вмешательство/речь» на эпизод.
--vqa.vqa_emission_hz1.0Как часто генерируются пары VQA.
--vqa.restrict_to_default_camerafalseОсновайте VQA только на --vlm.camera_key (иначе на каждой камере).
--vqa.coordinate_scale1000Сетка bbox/ключевых точек VLM, хранится в диапазоне [0, 1] (none = px).
--executor.episode_parallelism16Эпизоды обрабатываются параллельно в рамках каждой фазы.

Добавление новых модулей

Пайплайн создан для развития, и вклад сообщества очень приветствуется — это может быть совершенно новый модуль (например, трассировка траекторий или affordances), новый шаблон промпта, более умный процесс grounding или исправления качества в существующих модулях plan / interjections / vqa.

Каждый модуль находится в src/lerobot/annotations/steerable_pipeline/modules/, использует общий клиент VLM и кэш ключевых кадров, записывает свои необработанные результаты в дерево промежуточных данных и подключается к исполнителю как отдельная фаза. Есть идея? Создайте issue или PR в репозитории.

Как рецепты используют выход

Аннотации предназначены для чтения рецептами (см. Языковые столбцы и рецепты). Как правило:

  • Ветви low-level / high-level / memory-update читают subtask / plan / memory из language_persistent.
  • ветвь «вставка-ответ» читает события interjection вместе с соответствующим атомом речи (объединёнными в один ход ассистента через tool_calls_from) и обновлением plan с тем же временным меткой.
  • ветвь VQA считывает пары (vqa, user) и (vqa, assistant) из language_events.

Почему состояние и события разделены

Две идеи определяют дизайн:

  1. Состояние и точные события. Строки состояния (subtask, plan, memory) действуют на протяжении всего эпизода и отвечают на вопрос «что верно прямо сейчас?». Строки событий (interjection, vqa, речь) появляются только на том кадре, чье время совпадает. Метки времени копируются напрямую из исходного parquet — никогда не пересчитываются в формате с плавающей точкой.
  2. Один проход VLM. Все три модуля используют один и тот же клиент VLM (OpenAI-совместимый клиент, работающий с сервером vLLM задачи), поэтому загрузку модели вы оплачиваете один раз на датасет, а не три.

Повторный запуск одного модуля

Каждый модуль записывает свой необработанный вывод в <root>/.annotate_staging/episode_{N:06d}/<module>.jsonl. Это делает итерации над промптами недорогими: повторный запуск одного модуля перезаписывает только его собственный JSONL, после чего writer собирает итоговый parquet. Модули, которые не нужны, можно отключить с помощью --plan.enabled=false (аналогично --interjections.enabled / --vqa.enabled), чтобы тестировать их по одному.

Что проверяет валидатор

Перед запуском writer StagingValidator проверяет:

  • каждая строка события точно соответствует реальному временному метке кадра;
  • не остаётся сиротских пар «речь / междометие»;
  • plan обновляется в каждой временной метке прерывания;
  • memory строки попадают на границы подзадач (предупреждение, а не ошибка);
  • каждый ответ VQA-ассистента content представляет собой корректный JSON в одном из форматов bbox / keypoint / count / attribute / spatial;
  • каждая строка попадает в столбец, выбранный с помощью column_for_style(style).

Любая ошибка прерывает работу писателя. Для отладки передайте --skip_validation=true, чтобы отключить проверку.

Откуда берутся идеи каждого модуля

  • plan — подзадачи. Hi Robot (Shi 2025) для атомарной детализации («подними один лист салата», «положи миску в коробку»); Pi0.7 (Physical Intelligence 2025) для деталей «как, а не что».
  • plan — память. MEM (Torne 2026): сохраняйте только минимально релевантную информацию — фиксируйте результаты, исключайте конкретные атрибуты.
  • interjections. Таксономия сценариев Hi Robot: отрицательная задача, ситуативная коррекция, конкретное ограничение, предпочтение. Речь — это атом, содержащий только вызов инструмента (tool_calls=[{type:function, function:{name:"say", arguments:{text:...}}}]).
  • vqa. ECoT (Zawalski 2024) для привязанных к изображению признаков (ограничивающих прямоугольников [x_min, y_min, x_max, y_max] и ключевых точек, хранящихся в виде долей изображения в диапазоне [0, 1]) и Steerable VLA Policies (Zhao 2025) для привязки на нескольких уровнях абстракции. Pi0.7 также привязывает ответы на разных уровнях абстракции.

При доработке модуля изменяйте его шаблон промпта в src/lerobot/annotations/steerable_pipeline/prompts/, а не переписывайте его с нуля.

Примерная стоимость

В каждом эпизоде конвейер выполняет примерно plan_max_steps вызовов планирования, max_interjections_per_episode вызовов прерываний и vqa_emission_hz × episode_seconds вызовов VQA. При значениях по умолчанию (8 подзадач, 3 прерывания, 1 Гц × 3 пары) на 30-секундном эпизоде это составляет ~50 вызовов VLM.

Объём хранилища остаётся небольшим: language_persistent занимает не более десятков КБ на эпизод (parquet использует словарное кодирование для одной записи, повторяющейся между кадрами), а language_events пуст на большинстве кадров — его размер зависит от количества событий, а не от num_frames × num_emissions.

Update on GitHub