lerobot-annotate 使用视觉-语言模型(VLM)观看每个 episode 的视频,并将自然语言标注写回你的 dataset。它会从语言列与配方页面填充两个语言列——language_persistent 和 language_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)三个模块(plan、interjections、vqa)都与一个共享的 VLM 通信。每个模块将其输出暂存到磁盘,验证器对其进行检查,然后由单个写入器就地重写 dataset 分片。
每个模块发出几种类型的标注(“样式”),路由到两个语言列之一:
| 样式 / 原子 | 列 | 模块 |
|---|---|---|
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 询问子任务。相反,它使用两步的描述 → 分割流程:
两遍处理都将 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——工具文档将这些部分标记为尚未实现。
标注真实 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 作业,该作业会:
vllm/vllm-openai 镜像启动,并在其上安装 lerobot,plan / interjections / vqa 模块,--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.target | local | 要运行的 HF Jobs 配置(例如 h200、h200x4)。省略/local 则在本地运行。 |
--job.image | vllm/vllm-openai:latest | pod 的运行时镜像。 |
--job.timeout | 2h | 挂钟时间上限。对于大型 dataset 请提高该值。 |
--job.detach | false | 提交后退出,而不是流式传输日志。 |
--job.lerobot_ref | main | pod 上安装的 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 进行了调优。
| 参数 | 默认值 | 作用 |
|---|---|---|
--repo_id | — | 要标注的 Hub dataset(如果未设置 --root 则下载)。 |
--root | — | 改为标注本地 dataset 目录。 |
--new_repo_id | — | 将结果推送到新仓库(保持源仓库不变)。 |
--push_to_hub | false | 标注后上传(上传到 --new_repo_id,否则回到 --repo_id)。 |
--only_episodes | all | 仅标注这些 episode index(便于测试运行)。 |
--seed | 1729 | 为选择插话时间戳 + VQA 问题类型的随机数生成器设置种子。 |
默认情况下每个模块都开启,可以独立切换(设为 false 可跳过它,例如一次只迭代一个模块):
| 参数 | 默认值 | 关闭 |
|---|---|---|
--plan.enabled | true | 子任务 + 计划 + 记忆 + task_aug |
--interjections.enabled | true | 插话 + 语音原子 |
--vqa.enabled | true | VQA 对 |
| 参数 | 默认值 | 作用 |
|---|---|---|
--vlm.model_id | Qwen/Qwen3.6-27B | 要服务并提示的模型。 |
--vlm.camera_key | first 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 | 转发给 OpenAI 兼容服务器的思考预算提示(low/medium/high)。 |
| 参数 | 默认值 | 作用 |
|---|---|---|
--plan.frames_per_second | 2.0 | 缩略图集的帧采样率(2.0 = 每 0.5 秒一帧)。 |
--plan.max_frames_per_prompt | 60 | 每次 VLM 调用的帧预算。采样超过此值的 episode 会以相同密度自动分窗,然后拼接。 |
--plan.contact_sheet_columns | 5 | 每个缩略图集网格的列数(contact_sheet_frames_per_sheet 个图块,时间为行优先)。 |
--plan.plan_max_steps | 8 | 每个 episode 的子任务数量上限。 |
--plan.subtask_describe_first | true | 运行描述→分割的接地处理过程(最佳子任务质量;每个 episode +1 次调用)。 |
--plan.subtask_seeded_relabel | false | 第二遍:根据每个子任务的前一个/当前/后一个缩略图集重新标注,并以第一个标签作为种子(每个子任务 +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 | 按原样使用 dataset 任务(off)、仅当缺失/过短时(if_short),或始终从视频重新推导(always)。 |
| 参数 | 默认值 | 作用 |
|---|---|---|
--interjections.max_interjections_per_episode | 3 | 每个 episode 插话/语音对的上限。 |
--vqa.vqa_emission_hz | 1.0 | VQA 对的发出频率。 |
--vqa.restrict_to_default_camera | false | 仅基于 --vlm.camera_key 进行 VQA(否则基于每个相机)。 |
--executor.episode_parallelism | 16 | 每个阶段内并发处理的 episode 数。 |
该流水线旨在不断扩展,非常欢迎贡献——全新的模块(例如轨迹追踪或可操作性)、新的提示模板、更智能的接地流程,或对现有 plan / interjections / vqa 模块的质量修复。
每个模块都位于 src/lerobot/annotations/steerable_pipeline/modules/ 下,共享 VLM 客户端和关键帧缓存,将其原始输出写入暂存树,并作为自己的阶段接入执行器。有想法?在仓库上提交 issue 或 PR。
这些标注旨在由配方读取(参见语言列与配方)。通常:
language_persistent 读取 subtask / plan / memory。interjection 事件以及配对的语音原子(通过 tool_calls_from 合并为一个助手 episode),以及同一时间戳处匹配的 plan 刷新。language_events 读取 (vqa, user) 和 (vqa, assistant) 对。两个理念塑造了这一设计:
subtask、plan、memory)适用于整个 episode,回答“当前什么是真的?”。事件行(interjection、vqa、语音)仅出现在时间戳匹配的那一帧上。时间戳直接从源 parquet 复制——绝不以浮点数重新计算。每个模块将其原始输出暂存到 <root>/.annotate_staging/episode_{N:06d}/<module>.jsonl。这使得提示迭代成本很低:重新运行一个模块只会覆盖它自己的 JSONL,然后写入器重新组合最终的 parquet。使用 --plan.enabled=false(以及同样地 --interjections.enabled / --vqa.enabled)禁用你不想要的模块,以便一次测试一个。
在写入器运行之前,StagingValidator 会确认:
plan 在每个插话时间戳处刷新;memory 行落在子任务边界上(是警告,不是错误);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。