Spaces:
Running
Running
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()
|