Download tools/backfill_report_grid.py from HuggingAI4Engineering/CADGenBench: direct link, hf CLI and curl.
- Browser
- Download file 9.61 kB
-
https://huggingface.co/spaces/HuggingAI4Engineering/CADGenBench/resolve/main/tools/backfill_report_grid.py
- Command line
-
hf download hf://spaces/HuggingAI4Engineering/CADGenBench/tools/backfill_report_grid.py
-
curl -L -o backfill_report_grid.py https://huggingface.co/spaces/HuggingAI4Engineering/CADGenBench/resolve/main/tools/backfill_report_grid.py
9.61 kB
| #!/usr/bin/env python3 | |
| """Backfill the thumbnail-grid summary view into already-published reports. | |
| The report generator (``cadgenbench.eval.report.single_run``) now renders the | |
| summary view as a grouped thumbnail grid instead of a flat table. Reports | |
| produced before that change are static HTML files in the submissions dataset | |
| (``reports/<id>.html``); changing the generator does nothing to them. This | |
| one-time tool rewrites those published reports **in place, without re-evaluating | |
| or regenerating from run dirs**: | |
| - it reads each report's existing summary table (sample number, status, CAD | |
| score) and detail cards (which fixtures are editing) plus the render-bucket | |
| base URL already embedded in the file; | |
| - rebuilds the summary view as the grid using the *shared* builders from | |
| ``single_run`` (so a backfilled report is byte-identical to a freshly | |
| generated one), pointing editing cards at the ``edit_diff.png`` still and | |
| generation cards at the output ``iso.png`` — all assets that already exist; | |
| - injects the shared grid CSS/JS; the detail cards, header, score text and | |
| download button are left untouched. | |
| Run on local files (writes alongside, good for eyeballing):: | |
| python tools/backfill_report_grid.py --files /tmp/report.html -o /tmp/out.html | |
| Rewrite every published report in the submissions dataset (needs a write token):: | |
| HF_TOKEN=<write-token> python tools/backfill_report_grid.py --dataset | |
| python tools/backfill_report_grid.py --dataset --dry-run # list only | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import re | |
| import sys | |
| from pathlib import Path | |
| from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download | |
| # cadgenbench (for the shared grid builders) must be importable. | |
| _REPO_ROOT = Path(__file__).resolve().parents[2] | |
| _SRC = _REPO_ROOT / "cadgenbench" / "src" | |
| if _SRC.is_dir(): | |
| sys.path.insert(0, str(_SRC)) | |
| from cadgenbench.eval.report.single_run import ( # noqa: E402 | |
| _GRID_CSS, | |
| _GRID_JS, | |
| _render_grid_controls, | |
| grid_card_html, | |
| render_grid_groups, | |
| ) | |
| HF_ORG = os.getenv("HF_ORG", "HuggingAI4Engineering") | |
| SUBMISSIONS_REPO = os.getenv("HF_SUBMISSIONS_REPO", f"{HF_ORG}/cadgenbench-submissions") | |
| INPUT_PROXY_BASE = "/task-input" | |
| EDIT_DIFF_STILL = "edit_diff.png" | |
| # --- parsing the old flat-table report ------------------------------------- | |
| _RENDER_BASE_RE = re.compile( | |
| r'(https?://[^\s"\']+?/resolve/renders/[^/"\']+)/[^/"\']+/[^"\']+\.(?:png|webp)' | |
| ) | |
| _ROW_RE = re.compile( | |
| r'<tr class="q-[a-z]+" onclick="showDetail\((\d+)\)"[^>]*>(.*?)</tr>', re.S | |
| ) | |
| _NAME_RE = re.compile(r"<td>([^<]+)</td>") | |
| _STATUS_RE = re.compile(r'status-pill status-\w+">([^<]+)<') | |
| _CAD_RE = re.compile(r'<td data-v="([^"]+)"><b>') | |
| _SUMMARY_VIEW_RE = re.compile(r'(<div id="summary-view">).*?(</div>)', re.S) | |
| _GRID_HELP = ( | |
| '<p class="grid-help">Click a card to view details. ' | |
| '<span class="kbd">j</span>/<span class="kbd">k</span> to navigate, ' | |
| '<span class="kbd">Esc</span> to return. Each card shows the input and the ' | |
| "candidate output. Score tint: " | |
| "<span class='gtint q-high'>≥0.90</span> " | |
| "<span class='gtint q-mid'>≥0.60</span> " | |
| "<span class='gtint q-low'><0.60</span> CAD score.</p>" | |
| ) | |
| _INPUT_SHAPE_RE = re.compile(re.escape(INPUT_PROXY_BASE) + r"/[^\"']+/renders/") | |
| def _editing_idxs(doc: str) -> set[int]: | |
| """Indices whose detail card is an editing task (has a STEP input). | |
| Detected by the Input column showing the *starting shape's* renders | |
| (``/task-input/<fixture>/renders/...``), which the report emits for every | |
| editing sample because it derives from the ``input.step`` input. This is | |
| deliberately not keyed on the edit-diff turntable / ``(edit diff)`` heading: | |
| the old generator rendered an *invalid* editing candidate with the | |
| generation layout (no diff), so those markers miss invalid edits, whereas | |
| the starting-shape renders are always present. Matches the new generator's | |
| ``wants_shape`` grouping so a backfilled report and a freshly generated one | |
| classify identically. | |
| """ | |
| out: set[int] = set() | |
| for block in doc.split('<div class="fixture-card"')[1:]: | |
| m = re.match(r'\s*data-idx="(\d+)"', block) | |
| if m and _INPUT_SHAPE_RE.search(block): | |
| out.add(int(m.group(1))) | |
| return out | |
| def rewrite_report_html(doc: str) -> str | None: | |
| """Return the report rewritten with the grid summary view, or ``None``. | |
| ``None`` means "leave unchanged": the report is already a grid, or it | |
| isn't a hosted report we can rebuild (no render-bucket URL to point the | |
| output thumbnails at).""" | |
| if 'class="ggrid"' in doc or 'id="groups"' in doc: | |
| return None # already backfilled | |
| base_m = _RENDER_BASE_RE.search(doc) | |
| if not base_m: | |
| return None # not a hosted report (e.g. base64-inlined local report) | |
| render_base = base_m.group(1) | |
| edit_idxs = _editing_idxs(doc) | |
| gen_cards: list[str] = [] | |
| edit_cards: list[str] = [] | |
| for m in _ROW_RE.finditer(doc): | |
| idx = int(m.group(1)) | |
| cells = m.group(2) | |
| name_m = _NAME_RE.search(cells) | |
| if not name_m: | |
| continue | |
| name = name_m.group(1).strip() | |
| status_m = _STATUS_RE.search(cells) | |
| status = status_m.group(1).strip() if status_m else "?" | |
| cad_m = _CAD_RE.search(cells) | |
| cad: float | None = None | |
| if cad_m: | |
| try: | |
| v = float(cad_m.group(1)) | |
| cad = v if v >= 0 else None | |
| except ValueError: | |
| cad = None | |
| is_editing = idx in edit_idxs | |
| if is_editing: | |
| in_src = f"{INPUT_PROXY_BASE}/{name}/renders/iso.png" | |
| out_src = f"{render_base}/{name}/{EDIT_DIFF_STILL}" | |
| else: | |
| in_src = f"{INPUT_PROXY_BASE}/{name}/input.png" | |
| out_src = f"{render_base}/{name}/iso.png" | |
| card = grid_card_html( | |
| idx=idx, name=name, is_editing=is_editing, status=status, | |
| cad=cad, in_src=in_src, out_src=out_src, | |
| ) | |
| (edit_cards if is_editing else gen_cards).append(card) | |
| if not gen_cards and not edit_cards: | |
| return None | |
| new_inner = _GRID_HELP + _render_grid_controls() + render_grid_groups( | |
| gen_cards, edit_cards, | |
| ) | |
| if not _SUMMARY_VIEW_RE.search(doc): | |
| return None | |
| doc = _SUMMARY_VIEW_RE.sub( | |
| lambda mm: mm.group(1) + new_inner + "</div>", doc, count=1, | |
| ) | |
| # Inject the shared grid styles + filtering behavior. | |
| doc = doc.replace("</style>", _GRID_CSS + "</style>", 1) | |
| doc = doc.replace("</body>", f"<script>{_GRID_JS}</script></body>", 1) | |
| return doc | |
| def _run_files(files: list[Path], out: Path | None) -> int: | |
| for f in files: | |
| doc = f.read_text() | |
| new = rewrite_report_html(doc) | |
| if new is None: | |
| print(f" SKIP {f} (already grid / not a hosted report)") | |
| continue | |
| dest = out or f | |
| dest.write_text(new) | |
| print(f" wrote {dest} ({len(new) // 1024} KB)") | |
| return 0 | |
| def _run_dataset(api: HfApi, token: str | None, dry_run: bool, limit: int | None) -> int: | |
| files = [ | |
| f for f in api.list_repo_files(SUBMISSIONS_REPO, repo_type="dataset") | |
| if f.startswith("reports/") and f.endswith(".html") | |
| ] | |
| files.sort() | |
| if limit is not None: | |
| files = files[:limit] | |
| print(f"Found {len(files)} report(s) in {SUBMISSIONS_REPO}.") | |
| ops: list[CommitOperationAdd] = [] | |
| for i, rel in enumerate(files, start=1): | |
| local = hf_hub_download( | |
| repo_id=SUBMISSIONS_REPO, filename=rel, repo_type="dataset", token=token, | |
| ) | |
| new = rewrite_report_html(Path(local).read_text()) | |
| if new is None: | |
| print(f" [{i}/{len(files)}] SKIP {rel} (already grid / not hosted)") | |
| continue | |
| print(f" [{i}/{len(files)}] {rel} -> grid ({len(new) // 1024} KB)") | |
| if not dry_run: | |
| ops.append(CommitOperationAdd(path_in_repo=rel, path_or_fileobj=new.encode())) | |
| if dry_run: | |
| print(f"Dry run: would rewrite {len([f for f in files])} candidate(s).") | |
| return 0 | |
| if not ops: | |
| print("Nothing to rewrite.") | |
| return 0 | |
| if not token: | |
| print("HF_TOKEN required to commit.", file=sys.stderr) | |
| return 2 | |
| api.create_commit( | |
| repo_id=SUBMISSIONS_REPO, repo_type="dataset", operations=ops, | |
| commit_message="reports: backfill thumbnail-grid summary view", | |
| ) | |
| print(f"Committed {len(ops)} rewritten report(s) to {SUBMISSIONS_REPO}.") | |
| return 0 | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| src = parser.add_mutually_exclusive_group(required=True) | |
| src.add_argument("--files", nargs="+", type=Path, help="Local report HTML files.") | |
| src.add_argument( | |
| "--dataset", action="store_true", | |
| help="Rewrite every reports/*.html in the submissions dataset.", | |
| ) | |
| parser.add_argument("-o", "--output", type=Path, help="Output path (single --files).") | |
| parser.add_argument("--dry-run", action="store_true", help="List only (dataset mode).") | |
| parser.add_argument("--limit", type=int, default=None) | |
| args = parser.parse_args() | |
| if args.files: | |
| if args.output and len(args.files) != 1: | |
| parser.error("-o/--output only valid with a single --files argument.") | |
| return _run_files(args.files, args.output) | |
| token = os.environ.get("HF_TOKEN") | |
| return _run_dataset(HfApi(token=token), token, args.dry_run, args.limit) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |