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 = [ "
", ] for title, segments, klass, hit_indices in rows: parts.append(f"
{html.escape(title)}
") for idx, segment in enumerate(segments): left = max(0.0, min(100.0, 100 * segment.start_sec / duration)) width = max(0.4, min(100.0 - left, 100 * (segment.end_sec - segment.start_sec) / duration)) status = "hit" if idx in hit_indices and klass == "pred" else "miss" if klass == "gold": status = "" label = html.escape(f"{idx}: {segment.label} ({segment.start_sec:.1f}-{segment.end_sec:.1f}s)") parts.append( f"
{label}
" ) parts.append("
") parts.append("
") return "".join(parts) def segment_table(segments: list[Segment], kind: str, matched_pred: set[int] | None = None) -> list[list[Any]]: matched_pred = matched_pred or set() rows = [] for idx, segment in enumerate(segments): matched = idx in matched_pred if kind == "pred" else "" rows.append([idx, segment.start_sec, segment.end_sec, segment.end_sec - segment.start_sec, segment.label, matched]) return rows def judgment_table(judgments: list[dict[str, Any]]) -> list[list[Any]]: rows = [] for item in judgments: rows.append( [ item.get("pred_segment_index"), item.get("gt_segment_index"), item.get("temporal_iou"), item.get("semantic_match"), item.get("pred_subtask"), item.get("gt_subtask"), item.get("judge_raw_output"), ] ) rows.sort(key=lambda row: (row[0] if row[0] is not None else 9999, row[1] if row[1] is not None else 9999)) return rows def load_episode(rows: list[dict[str, Any]], choice: str | None, frame_index: int = 0) -> tuple[Any, ...]: if not rows: return empty_episode("No results loaded.") idx = max(0, min(parse_choice(choice), len(rows) - 1)) pred_row = rows[idx] bench_id = pred_row.get("bench_id") if not bench_id: return empty_episode("Selected prediction row has no bench_id.") gold_row = load_gold_row(str(bench_id)) video_path = write_video(gold_row) gold = gold_segments(gold_row) pred = predicted_segments(pred_row) judgments = candidate_judgments(pred_row) score, matched = episode_score(gold, pred, judgments) matched_pred = {pred_index for pred_index, _ in matched} duration = float(gold_row.get("duration_sec") or gold_row.get("num_frames") or 1) fps = float(gold_row.get("fps") or 10.0) max_frame = max(0, int(gold_row.get("num_frames") or 1) - 1) frame_index = max(0, min(int(frame_index or 0), max_frame)) time_sec = frame_index / fps if fps else 0.0 active = { "frame_index": frame_index, "time_sec": time_sec, "gold_subtask": segment_at(gold, time_sec), "predicted_subtask": segment_at(pred, time_sec), } summary = { "bench_id": bench_id, "source_dataset": gold_row.get("source_dataset"), "status": pred_row.get("status"), "error": pred_row.get("error"), "gold_instruction": gold_row.get("instruction"), "predicted_instruction": pred_row.get("predicted_instruction"), "annotation_model": pred_row.get("annotation_model"), "judge_model": pred_row.get("judge_model"), "score": score.__dict__, } status = ( f"{idx + 1}/{len(rows)} | {bench_id} | " f"F1={score.f1:.3f}, P={score.precision:.3f}, R={score.recall:.3f}, " f"matches={score.matches}/{score.gold}, pred={score.predicted}" ) slider = gr.Slider(value=frame_index, minimum=0, maximum=max_frame, step=1) return ( video_path, status, summary, timeline_html(gold, pred, matched, duration), segment_table(gold, "gold"), segment_table(pred, "pred", matched_pred), judgment_table(judgments), slider, render_frame(video_path, frame_index), active, pred_row.get("raw_annotation_output") or "", ) def empty_episode(message: str) -> tuple[Any, ...]: return ( None, message, {}, "", [], [], [], gr.Slider(value=0, minimum=0, maximum=1, step=1), None, {}, "", ) def step_episode(rows: list[dict[str, Any]], choice: str | None, delta: int) -> tuple[Any, ...]: items = choices(rows) if not items: return (gr.Dropdown(choices=[], value=None),) + empty_episode("No results loaded.") idx = max(0, min(parse_choice(choice) + delta, len(items) - 1)) return (gr.Dropdown(choices=items, value=items[idx]),) + load_episode(rows, items[idx], 0) def render_selected_frame(rows: list[dict[str, Any]], choice: str | None, frame_index: int) -> tuple[np.ndarray | None, dict[str, Any]]: if not rows: return None, {} idx = max(0, min(parse_choice(choice), len(rows) - 1)) pred_row = rows[idx] gold_row = load_gold_row(str(pred_row["bench_id"])) video_path = write_video(gold_row) fps = float(gold_row.get("fps") or 10.0) gold = gold_segments(gold_row) pred = predicted_segments(pred_row) max_frame = max(0, int(gold_row.get("num_frames") or 1) - 1) frame_index = max(0, min(int(frame_index or 0), max_frame)) time_sec = frame_index / fps if fps else 0.0 return render_frame(video_path, frame_index), { "frame_index": frame_index, "time_sec": time_sec, "gold_subtask": segment_at(gold, time_sec), "predicted_subtask": segment_at(pred, time_sec), } def step_frame(rows: list[dict[str, Any]], choice: str | None, frame_index: int, delta: int) -> tuple[gr.Slider, np.ndarray | None, dict[str, Any]]: if not rows: return gr.Slider(value=0, minimum=0, maximum=1, step=1), None, {} idx = max(0, min(parse_choice(choice), len(rows) - 1)) gold_row = load_gold_row(str(rows[idx]["bench_id"])) max_frame = max(0, int(gold_row.get("num_frames") or 1) - 1) next_frame = max(0, min(int(frame_index or 0) + delta, max_frame)) image, active = render_selected_frame(rows, choice, next_frame) return gr.Slider(value=next_frame, minimum=0, maximum=max_frame, step=1), image, active with gr.Blocks(title="Wasup Results Viewer") as demo: gr.Markdown("# Wasup Results Viewer") gr.Markdown("Compare Refiner prediction parquet against gold `macrodata/whats_going_on_bench` segments.") rows_state = gr.State([]) with gr.Row(): results_path = gr.Textbox(label="Results parquet/folder/HF prefix", value=DEFAULT_RESULTS, scale=5) load_results_btn = gr.Button("Load results", scale=1) load_status = gr.Textbox(label="Load status", interactive=False) episode = gr.Dropdown(label="Episode", choices=[], value=None) with gr.Row(): prev_btn = gr.Button("Previous") load_episode_btn = gr.Button("Load episode") next_btn = gr.Button("Next") video = gr.Video(label="Gold video") score_status = gr.Textbox(label="Episode score", interactive=False) metadata = gr.JSON(label="Episode metadata") timeline = gr.HTML(label="Gold/predicted timeline") with gr.Row(): gold_table = gr.Dataframe( label="Gold segments", headers=["idx", "start_sec", "end_sec", "duration", "subtask", "matched"], interactive=False, ) pred_table = gr.Dataframe( label="Predicted segments", headers=["idx", "start_sec", "end_sec", "duration", "subtask", "matched"], interactive=False, ) judgments = gr.Dataframe( label="Candidate semantic judgments", headers=["pred_idx", "gold_idx", "temporal_iou", "semantic_match", "pred_subtask", "gold_subtask", "judge_raw_output"], interactive=False, ) with gr.Row(): frame_prev = gr.Button("Previous frame") frame_next = gr.Button("Next frame") frame_slider = gr.Slider(label="Frame", minimum=0, maximum=1, value=0, step=1) frame_image = gr.Image(label="Selected frame", interactive=False) active_frame = gr.JSON(label="Subtask at selected frame") raw_output = gr.Textbox(label="Raw annotation output", lines=8, interactive=False) episode_outputs = [ video, score_status, metadata, timeline, gold_table, pred_table, judgments, frame_slider, frame_image, active_frame, raw_output, ] load_results_event = load_results_btn.click(load_results, inputs=results_path, outputs=[rows_state, episode, load_status]) load_results_event.then(load_episode, inputs=[rows_state, episode], outputs=episode_outputs) load_episode_btn.click(load_episode, inputs=[rows_state, episode], outputs=episode_outputs) episode.change(load_episode, inputs=[rows_state, episode], outputs=episode_outputs) prev_btn.click( lambda rows, choice: step_episode(rows, choice, -1), inputs=[rows_state, episode], outputs=[episode] + episode_outputs, ) next_btn.click( lambda rows, choice: step_episode(rows, choice, 1), inputs=[rows_state, episode], outputs=[episode] + episode_outputs, ) frame_slider.change(render_selected_frame, inputs=[rows_state, episode, frame_slider], outputs=[frame_image, active_frame]) frame_prev.click( lambda rows, choice, frame: step_frame(rows, choice, frame, -1), inputs=[rows_state, episode, frame_slider], outputs=[frame_slider, frame_image, active_frame], ) frame_next.click( lambda rows, choice, frame: step_frame(rows, choice, frame, 1), inputs=[rows_state, episode, frame_slider], outputs=[frame_slider, frame_image, active_frame], ) load_event = demo.load(load_results, inputs=results_path, outputs=[rows_state, episode, load_status]) load_event.then(load_episode, inputs=[rows_state, episode], outputs=episode_outputs) if __name__ == "__main__": demo.launch()