from __future__ import annotations import html import json import tempfile from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Any import cv2 import fsspec import gradio as gr import numpy as np import pyarrow.parquet as pq from huggingface_hub import HfApi, hf_hub_download GOLD_REPO = "macrodata/whats_going_on_bench" GOLD_PARQUET = "whats_going_on_bench.parquet" GOLD_INDEX = "whats_going_on_bench_index.parquet" GOLD_PARQUET_URL = f"https://huggingface.co/datasets/{GOLD_REPO}/resolve/main/{GOLD_PARQUET}" DEFAULT_RESULTS = ( "hf://datasets/macrodata/whats_going_on_runs/runs/" "smoke_gemini_lite_3_retry/subtask_segmentation_eval" ) IOU_THRESHOLD = 0.5 CACHE_DIR = Path(tempfile.gettempdir()) / "wasup_results_viewer" CACHE_DIR.mkdir(parents=True, exist_ok=True) @dataclass(frozen=True) class Segment: start_sec: float end_sec: float label: str def valid(self) -> bool: return bool(self.label.strip()) and self.end_sec > self.start_sec @dataclass(frozen=True) class EpisodeScore: precision: float recall: float f1: float matches: int predicted: int gold: int @lru_cache(maxsize=1) def load_gold_index() -> dict[str, dict[str, Any]]: path = hf_hub_download(repo_id=GOLD_REPO, repo_type="dataset", filename=GOLD_INDEX) rows = pq.read_table(path).to_pylist() return {row["bench_id"]: row for row in rows} @lru_cache(maxsize=1) def gold_parquet() -> pq.ParquetFile: fs = fsspec.filesystem("https", block_size=2**20) return pq.ParquetFile(fs.open(GOLD_PARQUET_URL, "rb")) @lru_cache(maxsize=512) def load_gold_row(bench_id: str) -> dict[str, Any]: index = load_gold_index() if bench_id not in index: raise KeyError(f"{bench_id} is not in {GOLD_REPO}") row_group = int(index[bench_id]["row_group"]) return gold_parquet().read_row_group(row_group).to_pylist()[0] def load_result_rows(source: str) -> tuple[list[dict[str, Any]], str]: source = (source or "").strip() or DEFAULT_RESULTS files = resolve_parquet_files(source) table = pq.read_table(files if len(files) > 1 else files[0]) rows = table.to_pylist() rows.sort(key=lambda row: (str(row.get("source_dataset") or ""), str(row.get("bench_id") or ""))) return rows, f"Loaded {len(rows)} prediction rows from {source}" def resolve_parquet_files(source: str) -> list[str]: if source.startswith("hf://datasets/"): return download_hf_parquets(source) path = Path(source).expanduser() if path.is_dir(): files = sorted(str(item) for item in path.rglob("*.parquet")) else: files = [str(path)] if not files: raise FileNotFoundError(f"No parquet files found at {source}") return files def download_hf_parquets(source: str) -> list[str]: suffix = source.removeprefix("hf://datasets/").strip("/") parts = suffix.split("/") if len(parts) < 2: raise ValueError("HF source must look like hf://datasets/owner/repo[/path]") repo_id = "/".join(parts[:2]) prefix = "/".join(parts[2:]).strip("/") api = HfApi() repo_files = api.list_repo_files(repo_id=repo_id, repo_type="dataset") if prefix: matches = [item for item in repo_files if item == prefix or item.startswith(prefix.rstrip("/") + "/")] else: matches = repo_files parquet_files = sorted(item for item in matches if item.endswith(".parquet")) if not parquet_files: raise FileNotFoundError(f"No parquet files found under {source}") return [ hf_hub_download(repo_id=repo_id, repo_type="dataset", filename=item) for item in parquet_files ] def choices(rows: list[dict[str, Any]]) -> list[str]: items = [] for idx, row in enumerate(rows): status = row.get("status") or "unknown" bench_id = row.get("bench_id") or "missing" source = row.get("source_dataset") or "unknown" instruction = row.get("instruction") or "" items.append(f"{idx:04d} | {status} | {source} | {bench_id} | {instruction}") return items def parse_choice(choice: str | None) -> int: if not choice: return 0 try: return int(choice.split("|", 1)[0].strip()) except Exception: return 0 def load_results(source: str) -> tuple[list[dict[str, Any]], gr.Dropdown, str]: rows, message = load_result_rows(source) items = choices(rows) value = items[0] if items else None return rows, gr.Dropdown(choices=items, value=value), message def write_video(row: dict[str, Any]) -> str: digest = str(row.get("primary_video_sha256") or row["bench_id"]) path = CACHE_DIR / f'{row["bench_id"]}_{digest[:12]}.mp4' if not path.exists(): path.write_bytes(row["primary_video"]) return str(path) def gold_segments(row: dict[str, Any]) -> list[Segment]: fps = float(row.get("fps") or 10.0) explicit = [] for item in row.get("subtasks") or []: segment = segment_from_mapping(item, fps=fps) if segment and segment.valid(): explicit.append(segment) if explicit: return explicit return collapse_frame_annotations(row.get("frame_annotations") or [], fps=fps) def segment_from_mapping(item: dict[str, Any], fps: float | None = None) -> Segment | None: label = str(item.get("subtask") or item.get("label") or item.get("text") or "").strip() try: if "start_sec" in item and "end_sec" in item: return Segment(float(item["start_sec"]), float(item["end_sec"]), label) if fps and "start_frame" in item and "end_frame" in item: return Segment(float(item["start_frame"]) / fps, float(item["end_frame"]) / fps, label) except (TypeError, ValueError): return None return None def collapse_frame_annotations(frames: list[dict[str, Any]], fps: float) -> list[Segment]: if not frames: return [] ordered = sorted(frames, key=lambda item: int(item.get("frame_index") or 0)) segments: list[Segment] = [] start = int(ordered[0].get("frame_index") or 0) previous = start label = str(ordered[0].get("subtask") or "") for item in ordered[1:]: frame = int(item.get("frame_index") or 0) next_label = str(item.get("subtask") or "") if next_label != label: segment = Segment(start / fps, (previous + 1) / fps, label) if segment.valid(): segments.append(segment) start = frame label = next_label previous = frame final = Segment(start / fps, (previous + 1) / fps, label) if final.valid(): segments.append(final) return segments def predicted_segments(row: dict[str, Any]) -> list[Segment]: raw = decode_json(row.get("predicted_subtasks_json"), []) segments = [] for item in raw: if isinstance(item, dict): segment = segment_from_mapping(item) if segment and segment.valid(): segments.append(segment) return sorted(segments, key=lambda item: (item.start_sec, item.end_sec)) def candidate_judgments(row: dict[str, Any]) -> list[dict[str, Any]]: raw = decode_json(row.get("candidate_judgments_json"), []) return [item for item in raw if isinstance(item, dict)] def decode_json(value: Any, default: Any) -> Any: if value is None: return default if isinstance(value, (list, dict)): return value if isinstance(value, bytes): value = value.decode("utf-8") if not isinstance(value, str) or not value.strip(): return default try: return json.loads(value) except json.JSONDecodeError: return default def temporal_iou(left: Segment, right: Segment) -> float: intersection = max(0.0, min(left.end_sec, right.end_sec) - max(left.start_sec, right.start_sec)) union = max(left.end_sec, right.end_sec) - min(left.start_sec, right.start_sec) return intersection / union if union > 0 else 0.0 def episode_score(gold: list[Segment], pred: list[Segment], judgments: list[dict[str, Any]]) -> tuple[EpisodeScore, set[tuple[int, int]]]: candidates: list[tuple[float, int, int]] = [] for item in judgments: if not item.get("semantic_match"): continue try: pred_index = int(item["pred_segment_index"]) gold_index = int(item["gt_segment_index"]) iou = float(item["temporal_iou"]) except (KeyError, TypeError, ValueError): continue if iou < IOU_THRESHOLD: continue if 0 <= pred_index < len(pred) and 0 <= gold_index < len(gold): candidates.append((iou, pred_index, gold_index)) candidates.sort(reverse=True) used_pred: set[int] = set() used_gold: set[int] = set() matched_pairs: set[tuple[int, int]] = set() for _, pred_index, gold_index in candidates: if pred_index in used_pred or gold_index in used_gold: continue used_pred.add(pred_index) used_gold.add(gold_index) matched_pairs.add((pred_index, gold_index)) matches = len(matched_pairs) precision = matches / len(pred) if pred else 0.0 recall = matches / len(gold) if gold else 0.0 f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0 return EpisodeScore(precision, recall, f1, matches, len(pred), len(gold)), matched_pairs def segment_at(segments: list[Segment], time_sec: float) -> str: for segment in segments: if segment.start_sec <= time_sec < segment.end_sec: return segment.label return "" def render_frame(video_path: str, frame_index: int) -> np.ndarray | None: capture = cv2.VideoCapture(video_path) if not capture.isOpened(): return None capture.set(cv2.CAP_PROP_POS_FRAMES, int(frame_index or 0)) ok, frame = capture.read() capture.release() if not ok or frame is None: return None return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) def timeline_html(gold: list[Segment], pred: list[Segment], matched: set[tuple[int, int]], duration: float) -> str: duration = max(duration, max([s.end_sec for s in gold + pred], default=1.0), 1.0) rows = [ ("Gold", gold, "gold", set(range(len(gold)))), ("Pred", pred, "pred", {pred_index for pred_index, _ in matched}), ] parts = [ "