Spaces:
Running on Zero
Running on Zero
| """ | |
| grading.py — canonical Kalanos RDQ metadata-only grading core. | |
| SINGLE SOURCE OF TRUTH for metadata-only ("0.1-meta") scoring. Imported by: | |
| - batch_grade.py (bulk grading of the launch list) | |
| - app.py (the Hugging Face Space on-demand grader) | |
| so that a dataset graded on the Space and the same dataset graded in the batch | |
| produce byte-identical *scores* — and, when HF returns the same metadata, | |
| byte-identical *reports*. All scoring logic lives here and nowhere else; do not | |
| re-implement any part of it in app.py or batch_grade.py. Changing a weight or a | |
| check here changes both surfaces at once. | |
| Metadata-only grade: `format` and `coverage` are MEASURED from repository | |
| metadata; `sync` and `outliers` are NOT measured and are scored null. The | |
| disclosure in `notes` must travel with the report — these grades are not a | |
| substitute for a measured (full) grade and must not be published publicly | |
| without that disclosure intact. | |
| """ | |
| import io | |
| import json | |
| import os | |
| import time | |
| from datetime import datetime, timezone | |
| import requests | |
| HF = "https://huggingface.co" | |
| GRADER_VERSION = "0.1-meta" | |
| REPORT_KIND = "metadata" | |
| # Default hard cap for the repo-size tree walk. batch_grade.py keeps the full | |
| # 45s (throughput over latency); the Space passes a smaller value so an | |
| # on-demand grade of a huge repo can't hang the UI (size never affects scores). | |
| SIZE_WALK_DEADLINE_S = 45 | |
| def UTC(): | |
| return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") | |
| def _auth_headers(): | |
| """Attach an HF token only if one is present in the environment. | |
| Public datasets resolve fine without it; this keeps the free public Space | |
| working with no secret set, while private/gated repos work when HF_TOKEN is.""" | |
| tok = os.environ.get("HF_TOKEN") | |
| return {"Authorization": f"Bearer {tok}"} if tok else {} | |
| def hf_api(path): | |
| r = requests.get(f"{HF}/api/{path}", timeout=30, headers=_auth_headers()) | |
| if r.status_code == 404: | |
| return None | |
| r.raise_for_status() | |
| return r.json() | |
| def hf_file(slug, path): | |
| r = requests.get(f"{HF}/datasets/{slug}/resolve/main/{path}", timeout=60, headers=_auth_headers()) | |
| if r.status_code in (401, 403, 404): | |
| return None | |
| r.raise_for_status() | |
| return r | |
| def fetch_repo_size_bytes(slug, deadline_s=SIZE_WALK_DEADLINE_S): | |
| """Repo size in bytes. Returns (size_bytes, complete): | |
| - complete=True -> size_bytes is the real total (from usedStorage, or a tree | |
| walk that finished before the cap). | |
| - complete=False, size_bytes not None -> LOWER BOUND: the tree walk hit the | |
| page/time cap before finishing (huge repos, e.g. droid_lerobot's ~1.7TB / | |
| tens of thousands of files). | |
| - complete=False, size_bytes is None -> couldn't determine size at all. | |
| Tries the dataset info endpoint's usedStorage first: one request regardless of | |
| repo size. Falls back to summing the paginated tree API only if that's absent, | |
| with a hard page/time cap so one huge repo can't stall the whole batch (or the | |
| interactive Space, which passes a shorter deadline_s).""" | |
| headers = _auth_headers() | |
| try: | |
| r = requests.get(f"{HF}/api/datasets/{slug}?expand[]=usedStorage", headers=headers, timeout=30) | |
| if r.status_code == 200: | |
| used = r.json().get("usedStorage") | |
| if isinstance(used, int) and used > 0: | |
| return used, True | |
| except Exception: | |
| pass | |
| url = f"{HF}/api/datasets/{slug}/tree/main?recursive=true" | |
| total = 0 | |
| pages = 0 | |
| deadline = time.time() + deadline_s # hard cap per dataset — large repos become a lower bound, not a hang | |
| try: | |
| while url and pages < 200 and time.time() < deadline: | |
| r = requests.get(url, headers=headers, timeout=20) | |
| if r.status_code == 404: | |
| return None, False | |
| r.raise_for_status() | |
| for item in r.json(): | |
| if item.get("type") == "file" and isinstance(item.get("size"), int): | |
| total += item["size"] | |
| url = r.links.get("next", {}).get("url") | |
| pages += 1 | |
| return total, (url is None) # url is None => walked the whole tree; else capped out | |
| except Exception: | |
| return (total or None), False | |
| def fetch_metadata(slug, size_deadline_s=SIZE_WALK_DEADLINE_S): | |
| """Real facts from HF: license, gated, and LeRobot meta if present. | |
| Returns None if the dataset does not exist (or the API 404s).""" | |
| info = hf_api(f"datasets/{slug}") | |
| if info is None: | |
| return None | |
| card = info.get("cardData") or {} | |
| meta = { | |
| "license": (card.get("license") if isinstance(card.get("license"), str) else (card.get("license") or [""])[0]) or "unknown", | |
| "gated": bool(info.get("gated")), | |
| "downloads": info.get("downloads", 0), | |
| "last_modified": info.get("lastModified", ""), | |
| "sha": (info.get("sha") or "")[:7], | |
| "lerobot": None, "episodes_meta": None, "size_bytes": None, "size_complete": False, | |
| } | |
| meta["size_bytes"], meta["size_complete"] = fetch_repo_size_bytes(slug, deadline_s=size_deadline_s) | |
| for path in ("meta/info.json",): | |
| r = hf_file(slug, path) | |
| if r is not None: | |
| try: | |
| meta["lerobot"] = r.json() | |
| except Exception: | |
| pass | |
| # episode stats (v2 jsonl or v3 parquet listing is heavy; jsonl is cheap when present) | |
| r = hf_file(slug, "meta/episodes.jsonl") | |
| if r is not None: | |
| lengths = [] | |
| for line in io.StringIO(r.text): | |
| try: | |
| lengths.append(json.loads(line).get("length", 0)) | |
| except Exception: | |
| pass | |
| if lengths: | |
| meta["episodes_meta"] = {"count": len(lengths), "lengths": lengths} | |
| return meta | |
| def hist(values, bins): | |
| counts = [0] * (len(bins) - 1) | |
| for v in values: | |
| for i in range(len(bins) - 1): | |
| if bins[i] <= v < bins[i + 1]: | |
| counts[i] += 1 | |
| break | |
| else: | |
| counts[-1] += 1 | |
| return {"bins": bins, "counts": counts} | |
| def gini(xs): | |
| xs = sorted(x for x in xs if x >= 0) | |
| n = len(xs) | |
| s = sum(xs) | |
| if n == 0 or s == 0: | |
| return 0.0 | |
| g = 0.0 | |
| for i, x in enumerate(xs, 1): | |
| g += (2 * i - n - 1) * x | |
| return round(g / (n * s), 3) | |
| def meta_grade(slug, m): | |
| """Metadata-only v0 grade. Format & coverage are measured; sync & outliers are | |
| provisional placeholders (null) disclosed in the notes. Do NOT publish these | |
| publicly without the disclosure intact. | |
| `m` is the dict returned by fetch_metadata(). This is the ONE function whose | |
| output defines a metadata grade; both the batch and the Space call it.""" | |
| lr = m.get("lerobot") or {} | |
| fps = lr.get("fps", 0) | |
| total_eps = lr.get("total_episodes") or (m.get("episodes_meta") or {}).get("count") or 0 | |
| total_frames = lr.get("total_frames", 0) | |
| tasks = lr.get("total_tasks", 0) | |
| features = lr.get("features", {}) or {} | |
| cams = [k for k in features if "image" in k or features[k].get("dtype") in ("video", "image")] | |
| fmt_checks = { | |
| "has_lerobot_meta": lr != {}, | |
| "fps_declared": bool(fps), | |
| "features_declared": bool(features), | |
| "chunked_layout": bool(lr.get("data_path")), | |
| } | |
| fmt_score = 100.0 * sum(fmt_checks.values()) / len(fmt_checks) if lr else 40.0 | |
| lens = (m.get("episodes_meta") or {}).get("lengths") or [] | |
| secs = [l / fps for l in lens] if fps and lens else [] | |
| ep_hist = hist(secs, [0, 10, 20, 30, 45, 60, 90, 120]) if secs else {"bins": [0, 1], "counts": [total_eps]} | |
| task_balance = 0.0 # per-task counts not in cheap metadata; neutral | |
| cov_score = min(100.0, 30 + 8 * tasks + 6 * len(cams)) if lr else 50.0 | |
| hours = round(total_frames / fps / 3600, 1) if fps and total_frames else 0 | |
| # meta/info.json's codebase_version already includes a leading "v" (e.g. "v2.0"); | |
| # strip it before prepending our own so we never emit "LeRobot vv2.0". | |
| raw_version = str(lr.get("codebase_version", "")).strip() | |
| version = raw_version.lstrip("vV") | |
| fmt_label = f"LeRobot v{version}" if version else ("LeRobot" if lr else "unknown") | |
| size_bytes = m.get("size_bytes") | |
| size_complete = m.get("size_complete") | |
| size_gb = round(size_bytes / 1e9, 2) if isinstance(size_bytes, int) else 0 | |
| if not isinstance(size_bytes, int): | |
| size_note = ["Repo size could not be determined; size_gb=0 is a placeholder, not a measurement."] | |
| elif not size_complete: | |
| size_note = [f"Repo size is a LOWER BOUND ({size_gb} GB): the file listing was capped before finishing " | |
| f"(large repo). Re-run with a longer cap or rely on usedStorage for an exact total."] | |
| else: | |
| size_note = [] | |
| return { | |
| "report_id": "", | |
| "report_kind": REPORT_KIND, | |
| "grader_version": GRADER_VERSION, | |
| "graded_at": UTC(), | |
| "source": {"kind": "hf", "ref": slug, "url": f"{HF}/datasets/{slug}", | |
| "commit": m["sha"], "license": m["license"].lower(), "gated": m["gated"]}, | |
| "dataset": {"format": fmt_label, | |
| "episodes": total_eps, "hours": hours, | |
| "embodiment": lr.get("robot_type", "unknown"), | |
| "sensors": cams or ["unknown"], "size_gb": size_gb}, | |
| "scores": {"total": round((fmt_score + cov_score) / 2, 1), | |
| "sync": None, "coverage": round(cov_score, 1), | |
| "outliers": None, "format": round(fmt_score, 1)}, | |
| "metrics": { | |
| "sync": {"median_drift_ms": 0, "p95_drift_ms": 0, "max_drift_ms": 0, "episodes_over_10ms": 0, "dropped_frames_pct": 0}, | |
| "coverage": {"task_families": tasks, "environments": 0, "episodes_per_task_min": 0, | |
| "episodes_per_task_max": 0, "gini_task_balance": task_balance, "lighting_variation_score": 0}, | |
| "outliers": {"flagged_episodes": 0, "flagged_pct": 0, "categories": {}}, | |
| "format": {"schema_valid_pct": 100 if lr else 0, "calibration_present_pct": 0, | |
| "units_consistent": bool(lr), "timestamps_monotonic_pct": 0}, | |
| }, | |
| "histograms": {"drift_ms": {"bins": [0, 1], "counts": [0]}, "episode_length_s": ep_hist}, | |
| "flagged_sample": [], | |
| "notes": [ | |
| f"Metadata-level report: file structure, coverage and episode statistics are measured from repository metadata. " | |
| f"Timestamp synchronization and trajectory outliers are not yet measured and are not scored.", | |
| f"{total_eps:,} episodes, {tasks} task(s), {len(cams)} camera stream(s), fps={fps}.", | |
| f"HF downloads: {m['downloads']:,}; last modified {m['last_modified'][:10]}.", | |
| *size_note, | |
| ], | |
| } | |