LeRobot documentation
Конвейер аннотации
Конвейер аннотации
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_persistent | plan |
plan (начальный + обновление при вмешательстве) | language_persistent | plan |
memory (сжатие в стиле MEM) | language_persistent | plan |
task_aug (перефразировки задачи) | language_persistent | plan |
interjection | language_events | interjections |
атом вызова инструмента речи (style=null, say) | language_events | interjections |
vqa (пара «пользователь / ассистент») | language_events | vqa |
Как создаются подзадачи
Модуль plan не запрашивает у VLM подзадачи одним запросом. Вместо этого он использует двухэтапный процесс describe → segment:
- Описание — VLM описывает только то, что действительно видит на выбранной камере (без догадок о задаче).
- 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, которое:
- использует образ
vllm/vllm-openaiи устанавливает поверх негоlerobot, - запускает один сервер vLLM на каждый GPU и управляет им через OpenAI-совместимый API,
- запускает модули
plan/interjections/vqaна наборе данных, - с
--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.target | local | Вариант HF Jobs для запуска (например, h200, h200x4). Пропущено/local выполняется здесь. |
--job.image | vllm/vllm-openai:latest | Образ среды выполнения для пода. |
--job.timeout | 2h | Ограничение по фактическому времени. Увеличьте его для больших наборов данных. |
--job.detach | false | Отправить и выйти, а не транслировать журналы. |
--job.lerobot_ref | main | Git-ссылка на установленную на поде версию 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_hub | false | Загрузка после аннотирования (в --new_repo_id, иначе обратно в --repo_id). |
--only_episodes | все | Аннотируйте только эти индексы эпизодов (удобно для тестового запуска). |
--seed | 1729 | Инициализирует генераторы случайных чисел, которые выбирают временные метки вставок и типы вопросов VQA. |
Какие модули выполняются
Каждый модуль включён по умолчанию и может быть переключён независимо (установите false, чтобы пропустить его, например, чтобы работать с одним модулем за раз):
| Флаг | По умолчанию | Отключает |
|---|---|---|
--plan.enabled | true | subtasks + plan + memory + task_aug |
--interjections.enabled | true | междометия + речевые атомы |
--vqa.enabled | true | VQA-пары |
VLM ( --vlm.* )
| Флаг | По умолчанию | Что он делает |
|---|---|---|
--vlm.model_id | Qwen/Qwen3.6-27B | Модель для обслуживания и запрос. |
--vlm.camera_key | сначала images.* | Камера, на которую опирается каждый промпт. |
--vlm.serve_command | auto | Точная команда vllm serve … (укажите здесь размер TP, память GPU, --max-model-len). |
--vlm.parallel_servers | 1 | Независимые серверы для маршрутизации по кругу (по одному на каждый GPU). |
--vlm.num_gpus | 0 | GPU на сервер (0 = по одному). |
--vlm.client_concurrency | 16 | Запросы в процессе выполнения на всех серверах. |
--vlm.max_new_tokens | 512 | Лимит генерации на один вызов. |
--vlm.temperature | 0.2 | Температура выборки. |
--vlm.reasoning_effort | null | Подсказка thinking-budget (low/medium/high), передаваемая на серверы, совместимые с OpenAI. |
Подзадачи / план / память ( --plan.* )
| Флаг | По умолчанию | Что он делает |
|---|---|---|
--plan.frames_per_second | 2.0 | Частота выборки кадров для контактных листов (2.0 = один кадр каждые 0,5 с). |
--plan.max_frames_per_prompt | 60 | Бюджет кадров на вызов VLM. Эпизоды, выборка которых превышает этот лимит, автоматически обрабатываются с окнами той же плотности, а затем склеиваются. |
--plan.contact_sheet_columns | 5 | Столбцов в сетке контактного листа (contact_sheet_frames_per_sheet тайлов, строки по времени). |
--plan.plan_max_steps | 8 | Верхняя граница количества подзадач на эпизод. |
--plan.subtask_describe_first | true | Выполните проход describe→segment для привязки (наилучшее качество подзадачи; +1 вызов/эпизод). |
--plan.subtask_seeded_relabel | false | Второй проход: переметить каждый подзадачу по её контактным листам prev/current/next, используя начальную метку (+1 вызов/подзадача). |
--plan.subtask_relabel_frames | 5 | Кадров, равномерно отобранных на лист сегмента в проходе повторной разметки (используется только при subtask_seeded_relabel=true). |
--plan.emit_plan | true | Выводить нумерованные строки plan (false = только подзадачи и память). |
--plan.emit_memory | true | Выводить строки memory (false = только подзадачи и план); симметрично emit_plan. |
--plan.n_task_rephrasings | 10 | Сколько перефразировок task_aug выводить (0 отключает). |
--plan.derive_task_from_video | if_short | Используйте задачу датасета как есть (off), только когда она отсутствует/короткая (if_short) или всегда вычисляйте заново из видео (always). |
Междометия + VQA
| Флаг | По умолчанию | Что он делает |
|---|---|---|
--interjections.max_interjections_per_episode | 3 | Ограничение на количество пар «вмешательство/речь» на эпизод. |
--vqa.vqa_emission_hz | 1.0 | Как часто генерируются пары VQA. |
--vqa.restrict_to_default_camera | false | Основайте VQA только на --vlm.camera_key (иначе на каждой камере). |
--vqa.coordinate_scale | 1000 | Сетка bbox/ключевых точек VLM, хранится в диапазоне [0, 1] (none = px). |
--executor.episode_parallelism | 16 | Эпизоды обрабатываются параллельно в рамках каждой фазы. |
Добавление новых модулей
Пайплайн создан для развития, и вклад сообщества очень приветствуется —
это может быть совершенно новый модуль (например, трассировка траекторий или 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.
Почему состояние и события разделены
Две идеи определяют дизайн:
- Состояние и точные события. Строки состояния (
subtask,plan,memory) действуют на протяжении всего эпизода и отвечают на вопрос «что верно прямо сейчас?». Строки событий (interjection,vqa, речь) появляются только на том кадре, чье время совпадает. Метки времени копируются напрямую из исходного parquet — никогда не пересчитываются в формате с плавающей точкой. - Один проход 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.