File size: 6,077 Bytes
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edae745
 
 
 
 
 
 
 
 
 
d646dd7
 
edae745
 
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edae745
 
 
 
 
 
d646dd7
 
 
 
 
 
 
edae745
 
d646dd7
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""LeRobot Export — 把批次构建为标准 LeRobot v3 数据集。

真正导出由 app.lerobot_export.build_lerobot_dataset 完成(视频/深度/
传感器/标注 → meta/info.json + 分块 parquet)。本模块把该实现接入
工作流节点:跑完工作流,数据集先生成到 worker 临时目录,完成后交给系统级
导出缓存;不在项目根目录生成 processed/,也不把导出副本混入源数据集。
"""

from app.processing import ProcessingModule, JobContext, ArtifactRef, field
from app.processing.registry import register
from app.processing.theme import EXPORT_COLOR


@register
class LeRobotExportModule(ProcessingModule):
    slug = "lerobot_export"
    version = "1.0"
    category = "export"
    label = "LeRobot Export"
    icon = "ant-design:cloud-server-outlined"
    color = EXPORT_COLOR
    inputs = ({"key": "data", "label": "Exportable Data"},)
    outputs = ({"key": "dataset", "label": "Dataset"},)
    # 默认排除 UMI 触觉力矩阵:每帧 250×250×3(187500 个整数),实测占
    # canonical parquet 体积的九成以上,且 99.99% 是零 —— 训练用不到,
    # 却让单集导出从 2.5 秒涨到 57 秒(写盘 + 逐元素统计 + 结果包上传),
    # 结果 zip 也从几 MB 涨到几十 MB。要保留就把它从配置里删掉。
    _DEFAULT_EXCLUDE = ",".join((
        "observation.gripper_left_force_matrix",
        "observation.gripper_right_force_matrix",
    ))
    default_config = {"version": "v3.0", "split_ratio": 0.9,
                      "shard_size": 100000, "exclude_columns": _DEFAULT_EXCLUDE}
    config_schema = (
        field("version", "select", "Version", "v3.0", options=["v2.1", "v3.0"]),
        field("exclude_columns", "string", "Exclude columns (comma separated)",
              _DEFAULT_EXCLUDE),
    )
    execution_target = "worker"
    capabilities = ("dataset_export", "lerobot")

    def run(self, ctx: JobContext) -> dict[str, ArtifactRef] | None:
        if not ctx.incoming:
            ctx.skip("No upstream data — skipped")
        episode_id = str(ctx.job.get("episode_id") or "")
        if not episode_id:
            ctx.skip("No episode_id in job")

        from app.lerobot_export import build_lerobot_dataset
        split_ratio = float(ctx.config.get("split_ratio", 0.9))
        # 布局版本:config_schema 的 version 选项(v2.1/v3.0)直接驱动
        # 导出版本由卡片配置决定;源项目始终保持本地 canonical v2.1
        # 三目录结构,v3.0 只在系统级导出缓存中生成。
        version = str(ctx.config.get("version") or "v3.0")
        # 导出范围由工作流连接决定:输入卡片(视频)连到本节点 → 数据集
        # 包含该视频;没连的相机 → 默认不导出视频(传感器/标注仍全量)。
        include_video_keys = [
            r.source_key for r in ctx.incoming.values()
            if r.kind == "video" and r.source_key
        ]
        # 手部骨骼产物:处理节点(mediapipe_hand)连到本节点 → 一并写入
        # 数据集(连接驱动);没连 → 不写入。
        # 注意必须 resolve:ref.path 是相对 output_root 的,直接传给
        # build_lerobot_dataset 会因工作目录不同而读不到文件。
        hand_keypoints_paths = [
            str(p) for r in ctx.incoming.values()
            if r.kind == "hand_keypoints" and r.path
            for p in (ctx.resolve(r),) if p
        ]
        # 双目三角化产物(stereo_triangulate 连到本节点)→ hand_*_world
        # 变为米制 3D 坐标(优先于 2D 归一化坐标)。
        hand_3d_paths = [
            str(p) for r in ctx.incoming.values()
            if r.kind == "hand_3d" and r.path
            for p in (ctx.resolve(r),) if p
        ]
        # 右目(辅助视角)骨骼产物(stereo_triangulate 的 hand_3d#right
        # "#" 兄弟键,沿边自动透传)连到本节点 → 以 _rcam 后缀列组写入
        # 数据集(与主数据并存)。
        hand_3d_right_paths = [
            str(p) for r in ctx.incoming.values()
            if r.kind == "hand_3d#right" and r.path
            for p in (ctx.resolve(r),) if p
        ]
        # hand_3d 产物的 unit 语义(manifest 声明,如深度图抬升的 camera_meters)
        # → 透传给数据集 feature 声明(缺省保持 mediapipe_world_relative)。
        hand_3d_unit = next(
            (r.metadata.get("unit") for r in ctx.incoming.values()
             if r.kind == "hand_3d" and getattr(r, "metadata", None)
             and r.metadata.get("unit")),
            None)
        # 数据集先输出到 worker 临时目录,API 完成阶段会把导出产品放到
        # state/exports,不在项目根目录新增 processed/。
        # 排除列:配置里逗号分隔。留空 = 全量导出(旧行为)。
        exclude_columns = [
            name.strip() for name
            in str(ctx.config.get("exclude_columns", "") or "").split(",")
            if name.strip()
        ]
        dataset_dir = build_lerobot_dataset(episode_id, [episode_id],
                                            ctx.output_root, split_ratio,
                                            include_video_keys=include_video_keys,
                                            hand_keypoints_paths=hand_keypoints_paths,
                                            hand_3d_paths=hand_3d_paths,
                                            hand_3d_right_paths=hand_3d_right_paths,
                                            version=version,
                                            hand_3d_unit=hand_3d_unit,
                                            exclude_columns=exclude_columns)
        info = dataset_dir / "meta" / "info.json"
        if not info.exists():
            ctx.skip("Dataset build produced no meta/info.json")
        return {
            "dataset": ctx.ref("dataset", info, metadata={
                "root": str(dataset_dir.relative_to(ctx.output_root)) if dataset_dir != ctx.output_root else ".",
                "episode_id": episode_id,
            }),
        }