标注流水线

lerobot-annotate 使用视觉-语言模型(VLM)观看每个 episode 的视频,并将自然语言标注写回你的 dataset。它会从语言列与配方页面填充两个语言列——language_persistentlanguage_events——直接写入 data/chunk-*/file-*.parquet

简而言之:将它指向一个 LeRobot dataset,它会添加可供 policy 训练的子任务、计划、记忆、插话、语音和视觉问答。

整体结构

  your dataset                  lerobot-annotate
  (LeRobot v3.1)
        │
        ▼
  ┌─────────────────────────────────────────────────────┐
  │                    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)

三个模块(planinterjectionsvqa)都与一个共享的 VLM 通信。每个模块将其输出暂存到磁盘,验证器对其进行检查,然后由单个写入器就地重写 dataset 分片。

流水线生成的内容

每个模块发出几种类型的标注(“样式”),路由到两个语言列之一:

样式 / 原子模块
subtask(Pi0.7 风格“如何做,而非做什么”)language_persistentplan
plan(初始 + 插话时刷新)language_persistentplan
memory(MEM 风格压缩)language_persistentplan
task_aug(任务的改写)language_persistentplan
interjectionlanguage_eventsinterjections
语音工具调用原子(style=nullsaylanguage_eventsinterjections
vqa(用户 / 助手对)language_eventsvqa

子任务如何生成

plan 模块不会一次性向 VLM 询问子任务。相反,它使用两步的描述 → 分割流程:

  1. 描述 — VLM 只叙述它在所选相机中实际看到的内容(不对任务进行猜测)。
  2. 分割 — 将该描述反馈回去,VLM 将 episode 拆分为连续的原子子任务。

两遍处理都将 episode 视为带时间戳的缩略图集——以 frames_per_second(默认 0.5 秒)采样的帧被打包成 JPEG 网格,并将每帧的时间烧录在角落,因此 VLM 可以直接引用精确的边界时间。这在视觉 token 方面远比每帧一张图像便宜,因此采样可以保持密集;超过 max_frames_per_prompt 的 episode 会以相同的密度拆分为多个窗口并合并。两个提示还都带有因果事件边界定义(当物体被握住 / 被释放 / 到达新位置 / 盖子改变 state / 内容物移动时,新事件开始),以锐化切分点的位置。

可选地,第三遍种子重标注--plan.subtask_seeded_relabel)会使用每个片段的前一个/当前/后一个片段的缩略图集重新检查该区间,并以第一个标签作为先验对标签进行最小修正——它保持边界固定,只锐化措辞,代价是每个子任务增加一次调用。

然后将得到的区间拼接成一个无间隙的完整 episode 覆盖,因此每一帧都恰好有一个活动的子任务。有关生产环境设置(单相机、带时间戳的缩略图集、自动窗口化的子任务生成),请参阅在 Hugging Face Jobs 上运行

工具

写入器不会向 parquet 添加 tools 列。工具目录存放于 meta/info.json["tools"] 中(参见工具)。每次运行后,流水线会确保规范的 say 模式在该列表中,并保留你事先声明的任何工具。

想添加自己的工具?直接编辑 meta/info.json["tools"]——流水线会保留已有的任何内容。这会使工具对聊天模板可见,从而让模型学习生成该调用。实际执行生成调用的运行时层(Tool 协议 / src/lerobot/tools/ 下的 TOOL_REGISTRY)不属于此 PR——工具文档将这些部分标记为尚未实现。

在 Hugging Face Jobs 上运行

标注真实 dataset 需要足够大的 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. 为每个 GPU 启动一个 vLLM 服务器,并通过 OpenAI 兼容的 API 驱动它,
  3. 在整个 dataset 上运行 plan / interjections / vqa 模块,
  4. 使用 --push_to_hub=true 将结果上传到 --new_repo_id(如果你不设置该参数,则原地上传回 --repo_id)。

该命令会流式传输作业日志;Ctrl-C 会分离而不取消它。使用 hf jobs hardware 列出可用的配置及其价格。

Qwen3.6 默认启用思考,这会消耗标注器用于 JSON 回答的 token 预算——--vlm.chat_template_kwargs='{"enable_thinking": false}' 会将其关闭。如果没有 --push_to_hub=true,标注后的 dataset 会在 pod 退出时被丢弃。

作业选项

参数默认值作用
--job.targetlocal要运行的 HF Jobs 配置(例如 h200h200x4)。省略/local 则在本地运行。
--job.imagevllm/vllm-openai:latestpod 的运行时镜像。
--job.timeout2h挂钟时间上限。对于大型 dataset 请提高该值。
--job.detachfalse提交后退出,而不是流式传输日志。
--job.lerobot_refmainpod 上安装的 lerobot 的 Git ref——将其指向某个分支以测试更改。
--job.tags[]作业及其推送的任何 dataset 上的额外标签(始终会添加 lerobot)。

对于更大的 dataset,请扩展到 h200x4 并相应提高 --vlm.parallel_servers / --vlm.num_gpus,并使用例如 --job.timeout=8h 为作业提供更多余量。

远程运行需要 --repo_id(pod 会从 Hub 拉取 dataset;--root 指定的是只有你的机器才有的目录)。仅存在于本地缓存中的 dataset 会先被推送到私有仓库。

关键选项

这些是你最常使用的参数。运行 lerobot-annotate --help 查看其他所有内容;默认值针对短操作 episode 进行了调优。

dataset 输入 / 输出

参数默认值作用
--repo_id要标注的 Hub dataset(如果未设置 --root 则下载)。
--root改为标注本地 dataset 目录。
--new_repo_id将结果推送到新仓库(保持源仓库不变)。
--push_to_hubfalse标注后上传(上传到 --new_repo_id,否则回到 --repo_id)。
--only_episodesall仅标注这些 episode index(便于测试运行)。
--seed1729为选择插话时间戳 + VQA 问题类型的随机数生成器设置种子。

运行哪些模块

默认情况下每个模块都开启,可以独立切换(设为 false 可跳过它,例如一次只迭代一个模块):

参数默认值关闭
--plan.enabledtrue子任务 + 计划 + 记忆 + task_aug
--interjections.enabledtrue插话 + 语音原子
--vqa.enabledtrueVQA 对

VLM( --vlm.* )

参数默认值作用
--vlm.model_idQwen/Qwen3.6-27B要服务并提示的模型。
--vlm.camera_keyfirst images.*每个提示所依据的相机。
--vlm.serve_commandauto确切的 vllm serve … 命令(在此处设置 TP 大小、GPU 内存、--max-model-len)。
--vlm.parallel_servers1用于轮询路由的独立服务器(每个 GPU 一个)。
--vlm.num_gpus0每个服务器的 GPU 数(0 = 每个一个)。
--vlm.client_concurrency16所有服务器上的在途请求数。
--vlm.max_new_tokens512每次调用的生成上限。
--vlm.temperature0.2采样温度。
--vlm.reasoning_effortnull转发给 OpenAI 兼容服务器的思考预算提示(low/medium/high)。

子任务 / 计划 / 记忆( --plan.* )

参数默认值作用
--plan.frames_per_second2.0缩略图集的帧采样率(2.0 = 每 0.5 秒一帧)。
--plan.max_frames_per_prompt60每次 VLM 调用的帧预算。采样超过此值的 episode 会以相同密度自动分窗,然后拼接。
--plan.contact_sheet_columns5每个缩略图集网格的列数(contact_sheet_frames_per_sheet 个图块,时间为行优先)。
--plan.plan_max_steps8每个 episode 的子任务数量上限。
--plan.subtask_describe_firsttrue运行描述→分割的接地处理过程(最佳子任务质量;每个 episode +1 次调用)。
--plan.subtask_seeded_relabelfalse第二遍:根据每个子任务的前一个/当前/后一个缩略图集重新标注,并以第一个标签作为种子(每个子任务 +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按原样使用 dataset 任务(off)、仅当缺失/过短时(if_short),或始终从视频重新推导(always)。

插话 + VQA

参数默认值作用
--interjections.max_interjections_per_episode3每个 episode 插话/语音对的上限。
--vqa.vqa_emission_hz1.0VQA 对的发出频率。
--vqa.restrict_to_default_camerafalse仅基于 --vlm.camera_key 进行 VQA(否则基于每个相机)。
--executor.episode_parallelism16每个阶段内并发处理的 episode 数。

贡献新模块

该流水线旨在不断扩展,非常欢迎贡献——全新的模块(例如轨迹追踪或可操作性)、新的提示模板、更智能的接地流程,或对现有 plan / interjections / vqa 模块的质量修复。

每个模块都位于 src/lerobot/annotations/steerable_pipeline/modules/ 下,共享 VLM 客户端和关键帧缓存,将其原始输出写入暂存树,并作为自己的阶段接入执行器。有想法?在仓库上提交 issue 或 PR。

配方如何使用输出

这些标注旨在由配方读取(参见语言列与配方)。通常:

  • 低层 / 高层 / 记忆更新分支从 language_persistent 读取 subtask / plan / memory
  • 插话响应分支读取 interjection 事件以及配对的语音原子(通过 tool_calls_from 合并为一个助手 episode),以及同一时间戳处匹配的 plan 刷新。
  • VQA 分支从 language_events 读取 (vqa, user)(vqa, assistant) 对。

为什么将 state 和事件分开

两个理念塑造了这一设计:

  1. 持久 state 与精确事件。 持久行(subtaskplanmemory)适用于整个 episode,回答“当前什么是真的?”。事件行(interjectionvqa、语音)仅出现在时间戳匹配的那一帧上。时间戳直接从源 parquet 复制——绝不以浮点数重新计算。
  2. 一次 VLM 处理。 所有三个模块共享单个 VLM 客户端(与作业的 vLLM 服务器通信的 OpenAI 兼容客户端),因此你只需为每个 dataset 支付一次模型加载成本,而不是三次。

重新运行单个模块

每个模块将其原始输出暂存到 <root>/.annotate_staging/episode_{N:06d}/<module>.jsonl。这使得提示迭代成本很低:重新运行一个模块只会覆盖它自己的 JSONL,然后写入器重新组合最终的 parquet。使用 --plan.enabled=false(以及同样地 --interjections.enabled / --vqa.enabled)禁用你不想要的模块,以便一次测试一个。

验证器检查的内容

在写入器运行之前,StagingValidator 会确认:

  • 每个事件行都恰好落在真实的帧时间戳上;
  • 没有语音 / 插话对被孤立;
  • plan 在每个插话时间戳处刷新;
  • memory 行落在子任务边界上(是警告,不是错误);
  • 每个 VQA 助手 content 都是 bbox / 关键点 / 计数 / 属性 / 空间形状之一的有效 JSON;
  • 每一行都进入由 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]、关键点),Steerable VLA Policies(Zhao 2025)提供多抽象级别的接地。Pi0.7 也会跨抽象级别对答案进行接地。

改进模块时,在 src/lerobot/annotations/steerable_pipeline/prompts/ 中调整其提示模板,而不是从头重写。

大致成本

每个 episode,流水线大约进行 max_steps 次计划调用、max_interjections_per_episode 次插话调用和 vqa_emission_hz × episode_seconds 次 VQA 调用。在 30 秒的 episode 上使用默认值(8 个子任务、1 次插话、1 Hz × 3 对),约为 50 次 VLM 调用。

存储量保持很小:language_persistent 每个 episode 最多为几十 KB(parquet 对跨帧重复的那一个条目进行字典编码),而 language_events 在大多数帧上为空——其大小随发出次数扩展,而不是 num_frames × num_emissions

在 GitHub 上更新