File size: 7,676 Bytes
73856b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13f44ad
 
 
 
 
 
 
 
73856b4
 
 
 
 
 
 
 
 
 
 
 
 
 
13f44ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73856b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13f44ad
 
73856b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13f44ad
 
73856b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
"""Generate data.json for the G1 Moves showcase site.

Reads manifest.json + scans the filesystem to determine which media
files (GIF/MP4) exist for each clip at each pipeline stage.

Usage:
    python generate_data.py              # from space/ directory
    python generate_data.py --repo /path/to/g1-moves
"""

import argparse
import json
import re
from pathlib import Path

STAGES = ["capture", "retarget", "training", "policy"]
FEATURED_CLIPS = {"V_PullOver": 0}

# Descriptive subtitles for karate moves (from README)
KARATE_SUBTITLES = {
    "M_Move1": "Guard Combo",
    "M_Move2": "Low Punch",
    "M_Move3": "Horse Stance",
    "M_Move4": "Spin Punch",
    "M_Move5": "Twist Punch",
    "M_Move6": "Spin Strike",
    "M_Move7": "Rapid Punch",
    "M_Move8": "Drop Spin",
    "M_Move9": "Level Change",
    "M_Move10": "Side Kick",
    "M_Move11": "Blitz",
    "M_Move17": "Double Strike",
    "M_Move18": "Front Kick",
    "M_Move19": "Slow Kata",
    "M_Move20": "Open Strike",
    "M_ShortMove12": "Quick Jab",
    "M_ShortMove13": "Snap Kick",
    "M_ShortMove14": "Light Punch",
    "M_ShortMove15": "Drop Strike",
    "M_ShortMove16": "Power Burst",
}


def format_display_name(clip_id: str) -> str:
    """Convert clip ID to human-readable name."""
    name = re.sub(r"^[BJMV]_", "", clip_id)
    name = re.sub(r"([a-z\d])([A-Z])", r"\1 \2", name)
    name = re.sub(r"([A-Za-z])(\d)", r"\1 \2", name)
    name = name.replace("_", " ")
    return name


def get_media_file(clip_id: str, stage: str) -> str:
    """Return the expected GIF filename for a clip at a given stage."""
    if stage == "capture":
        return f"{clip_id}.gif"
    return f"{clip_id}_{stage}.gif"


def scan_stage(repo: Path, category: str, clip_id: str, stage: str) -> dict | None:
    """Check for media or pipeline artifacts at a pipeline stage.

    Visual GIF/MP4 previews are preferred. For newly added robot-compatible
    clips such as V_PullOver, some stages only have the real artifacts
    (CSV/PKL/NPZ/ONNX/PT) and no rendered preview yet. Return artifact
    metadata so the public page shows the stage as available instead of
    falsely saying "Not yet available".
    """
    stage_dir = repo / category / clip_id / stage
    result = {}

    gif_name = get_media_file(clip_id, stage)
    gif_path = stage_dir / gif_name
    if gif_path.exists():
        result["gif"] = f"{category}/{clip_id}/{stage}/{gif_name}"

    # Also check for MP4
    mp4_name = gif_name.replace(".gif", ".mp4")
    mp4_path = stage_dir / mp4_name
    if mp4_path.exists():
        result["mp4"] = f"{category}/{clip_id}/{stage}/{mp4_name}"

    artifacts = []
    if stage == "capture":
        # The manifest entry itself means the source clip was captured/imported,
        # even if the original phone/video file is not published in the dataset.
        result.setdefault("status", "Source captured/imported")
    elif stage == "retarget":
        for suffix, label in [(".csv", "Retarget CSV"), (".pkl", "Robot motion PKL")]:
            path = stage_dir / f"{clip_id}{suffix}"
            if path.exists():
                artifacts.append({"label": label, "url": f"{category}/{clip_id}/{stage}/{clip_id}{suffix}"})
    elif stage == "training":
        path = stage_dir / f"{clip_id}.npz"
        if path.exists():
            artifacts.append({"label": "Training NPZ", "url": f"{category}/{clip_id}/{stage}/{clip_id}.npz"})
    elif stage == "policy":
        for policy_dir, label_prefix in [("policy", "Browser policy"), ("policy_154", "Robot-compatible 154D policy")]:
            for suffix, label in [(".onnx", "ONNX"), (".pt", "PyTorch checkpoint")]:
                path = repo / category / clip_id / policy_dir / f"{clip_id}_policy{suffix}"
                if path.exists():
                    artifacts.append({"label": f"{label_prefix} {label}", "url": f"{category}/{clip_id}/{policy_dir}/{clip_id}_policy{suffix}"})

    if artifacts:
        result["artifacts"] = artifacts
        result.setdefault("status", "Artifacts available")

    return result if result else None


def main():
    parser = argparse.ArgumentParser(description="Generate site data from manifest")
    parser.add_argument(
        "--repo",
        type=Path,
        default=Path(__file__).parent.parent,
        help="Path to g1-moves repository root",
    )
    args = parser.parse_args()
    repo = args.repo.resolve()

    manifest_path = repo / "manifest.json"
    if not manifest_path.exists():
        print(f"ERROR: manifest.json not found at {manifest_path}")
        return

    manifest = json.loads(manifest_path.read_text())

    clips = []
    stats = {"dance": 0, "karate": 0, "bonus": 0, "policies": 0, "total": 0}

    for clip_id, info in sorted(manifest["clips"].items()):
        cat = info["category"]
        stats[cat] = stats.get(cat, 0) + 1
        stats["total"] += 1

        stages = {}
        for stage in STAGES:
            media = scan_stage(repo, cat, clip_id, stage)
            if media:
                stages[stage] = media

        # Check for browser-viewer policy files (legacy 160-dim) and robot policy_154 files.
        # policy_154 files are real-robot artifacts, but the current browser viewer still
        # expects the older media/<category>/<clip>/policy/<clip>_policy.onnx path.
        onnx_path = repo / cat / clip_id / "policy" / f"{clip_id}_policy.onnx"
        pt_path = repo / cat / clip_id / "policy" / f"{clip_id}_policy.pt"
        policy154_onnx_path = repo / cat / clip_id / "policy_154" / f"{clip_id}_policy.onnx"
        policy154_pt_path = repo / cat / clip_id / "policy_154" / f"{clip_id}_policy.pt"
        has_onnx = onnx_path.exists()
        has_pt = pt_path.exists()
        has_policy_154 = policy154_onnx_path.exists() or policy154_pt_path.exists()
        has_policy = "policy" in stages or has_onnx or has_pt or has_policy_154
        supports_viewer = has_onnx or has_policy_154
        viewer_policy = "policy" if has_onnx else ("policy_154" if has_policy_154 else None)
        if has_policy:
            stats["policies"] += 1

        subtitle = KARATE_SUBTITLES.get(clip_id)
        display_name = format_display_name(clip_id)
        if subtitle:
            display_name = f"{display_name}: {subtitle}"

        clips.append(
            {
                "id": clip_id,
                "name": display_name,
                "category": cat,
                "performer": info.get("performer", "Unknown"),
                "duration": info.get("duration_s", 0),
                "fps": info.get("fps", 60),
                "frames": info.get("frames", 0),
                "stages": stages,
                "has_policy": has_policy,
                "has_onnx": has_onnx,
                "has_policy_154": has_policy_154,
                "supports_viewer": supports_viewer,
                "viewer_policy": viewer_policy,
                "featured_rank": FEATURED_CLIPS.get(clip_id),
            }
        )

    # Sort featured clips first, then categories grouped, then alphabetical.
    cat_order = {"dance": 0, "karate": 1, "bonus": 2}
    clips.sort(key=lambda c: (
        c.get("featured_rank") if c.get("featured_rank") is not None else 999,
        cat_order.get(c["category"], 9),
        c["id"],
    ))

    data = {
        "base_url": "https://huggingface.co/datasets/exptech/g1-moves/resolve/main",
        "clips": clips,
        "stats": stats,
    }

    output = Path(__file__).parent / "data.json"
    output.write_text(json.dumps(data, indent=2))
    print(f"Generated {output.name}: {len(clips)} clips, {stats['policies']} policies")


if __name__ == "__main__":
    main()