Spaces:
Runtime error
Runtime error
File size: 8,816 Bytes
6e031e4 | 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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Pre-render the submission notebook so it shows everything on GitHub.
GitHub renders .ipynb statically — Mermaid code blocks inside markdown cells
show as plain code (no diagram), and code cells with no saved outputs look
blank. This script fixes both:
1. **Mermaid → PNG.** Each ```mermaid block is replaced with an 
reference. PNGs fetched from the public mermaid.ink renderer and saved to
notebooks/assets/diagrams/.
2. **Dataset previews + inference previews → executed outputs.** Re-executes
the cells that produce matplotlib grids / inference visualizations so the
.ipynb on disk ships with embedded PNG outputs that render on GitHub.
Idempotent. Re-run any time the notebook changes.
Usage:
python scripts/render_notebook_assets.py
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import re
import sys
import time
import ssl
import urllib.request
from pathlib import Path
try:
import certifi
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())
except ImportError:
_SSL_CTX = ssl.create_default_context()
ROOT = Path(__file__).resolve().parent.parent
NB_PATH = ROOT / "notebooks" / "ccdp_submission.ipynb"
DIAGRAMS_DIR = ROOT / "notebooks" / "assets" / "diagrams"
MERMAID_INK = "https://mermaid.ink/img/{b64}?type=png&bgColor=ffffff"
# Which code cells to execute and bake outputs for (matched by substring).
# Heavy / network-dependent cells are skipped — we only execute the cheap
# previewers that produce useful static images.
EXECUTE_IF_CONTAINS = (
"show_grid(samples[:8], 'CarDD val samples",
"show_grid(samples[:8], 'Stanford-Cars random",
"label_files = list(yolo_dir.glob", # YOLO label preview (text only, but still)
"model = YOLO(str(weights))", # damage_seg overlay preview (§3.3)
"print(f'Parts detected in", # parts_seg detection list (§4.1)
)
# Cells we never auto-execute (require uploaded image, slow, or interactive)
NEVER_EXECUTE_IF_CONTAINS = (
"from google.colab import files",
"estimate_multi(IMG_PATH)",
"files.upload()",
)
# ---------------------------------------------------------------------------
# Mermaid → PNG
# ---------------------------------------------------------------------------
def _mermaid_to_png(source: str, dest: Path) -> bool:
"""POST the Mermaid source to mermaid.ink and save the PNG. Returns True on success."""
# mermaid.ink uses url-safe base64 (no padding stripped in their decoder).
b64 = base64.urlsafe_b64encode(source.encode("utf-8")).decode("ascii")
url = MERMAID_INK.format(b64=b64)
try:
req = urllib.request.Request(url, headers={"User-Agent": "ccdp-build/0.1"})
with urllib.request.urlopen(req, timeout=30, context=_SSL_CTX) as r:
data = r.read()
if not data or len(data) < 200:
print(f" ! mermaid.ink returned {len(data)} bytes for {dest.name} — skipping")
return False
dest.write_bytes(data)
return True
except Exception as e:
print(f" ! mermaid.ink failed for {dest.name}: {e}")
return False
_MERMAID_BLOCK = re.compile(r"```mermaid\n(.*?)```", re.DOTALL)
def _stable_id(source: str, ix: int) -> str:
"""Deterministic name per diagram so re-runs don't churn the disk."""
h = hashlib.sha1(source.encode()).hexdigest()[:10]
return f"diagram_{ix:02d}_{h}"
def replace_mermaid_blocks(nb: dict) -> int:
"""Mutate notebook in place: replace each Mermaid fence with an image
reference (relative to the notebook). Return the count replaced."""
DIAGRAMS_DIR.mkdir(parents=True, exist_ok=True)
seen: dict[str, str] = {} # source -> rel path (dedupe identical diagrams)
ix = 0
n_replaced = 0
for cell in nb["cells"]:
if cell.get("cell_type") != "markdown":
continue
src = cell["source"] if isinstance(cell["source"], str) else "".join(cell["source"])
if "```mermaid" not in src:
continue
def _sub(match):
nonlocal ix, n_replaced
body = match.group(1).strip()
if body in seen:
return f""
name = _stable_id(body, ix) + ".png"
ix += 1
dest = DIAGRAMS_DIR / name
if not dest.exists():
ok = _mermaid_to_png(body, dest)
if not ok:
return match.group(0) # keep raw fence on failure
print(f" rendered {name} ({dest.stat().st_size/1024:.0f} KB)")
time.sleep(0.4) # be polite to mermaid.ink
else:
print(f" reuse {name}")
seen[body] = name
n_replaced += 1
return f""
new_src = _MERMAID_BLOCK.sub(_sub, src)
cell["source"] = new_src
return n_replaced
# ---------------------------------------------------------------------------
# Executing selected cells to bake outputs
# ---------------------------------------------------------------------------
def _cell_should_execute(src: str) -> bool:
if any(s in src for s in NEVER_EXECUTE_IF_CONTAINS):
return False
return any(s in src for s in EXECUTE_IF_CONTAINS)
def execute_preview_cells(nb_path: Path) -> int:
"""Execute the notebook full-through (allow_errors=True), then save with
embedded outputs. Cells that we know will fail (Colab uploads, the
git-clone install) are temporarily blanked to no-ops so they don't poison
state for the cells we DO want outputs from."""
try:
import nbformat
from nbclient import NotebookClient
except ImportError:
print(" ! nbclient/nbformat not installed — skipping cell execution")
print(" pip install nbclient nbformat ipykernel")
return 0
nb = nbformat.read(str(nb_path), as_version=4)
# Stub out cells that don't make sense in a non-Colab batch context.
n_stubbed = 0
for cell in nb.cells:
if cell.cell_type != "code":
continue
src = cell.source if isinstance(cell.source, str) else "".join(cell.source)
# The §1.1 install cell tries `pip install -e .` which we already did
# in the dev venv; let it run (it's a noop).
# Stub the upload widget + the download-from-release cell (slow on a
# cold machine; release weights already fetched if you ran §1.3 once).
if any(s in src for s in NEVER_EXECUTE_IF_CONTAINS):
cell.metadata.setdefault("ccdp", {})["original_source"] = cell.source
cell.source = "# (skipped during pre-render — runtime-only cell)"
n_stubbed += 1
print(f" stubbed {n_stubbed} runtime-only cells; executing the rest…")
client = NotebookClient(
nb, timeout=300, kernel_name="ccdp-dev",
resources={"metadata": {"path": str(ROOT)}},
allow_errors=True,
)
try:
client.execute()
except Exception as e:
print(f" ! execution error: {e}")
# Restore stubbed sources
for cell in nb.cells:
if cell.cell_type == "code" and "ccdp" in cell.metadata \
and "original_source" in cell.metadata["ccdp"]:
cell.source = cell.metadata["ccdp"].pop("original_source")
if not cell.metadata["ccdp"]:
cell.metadata.pop("ccdp")
cell.outputs = [] # don't show stub output
nbformat.write(nb, str(nb_path))
n_with_out = sum(1 for c in nb.cells if c.cell_type == "code" and c.get("outputs"))
print(f" cells with saved outputs: {n_with_out}")
return n_with_out
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--skip-diagrams", action="store_true", help="Skip Mermaid rendering")
ap.add_argument("--skip-execute", action="store_true", help="Skip cell execution")
args = ap.parse_args()
print(f"Reading {NB_PATH}")
nb = json.loads(NB_PATH.read_text())
if not args.skip_diagrams:
print("\n[1/2] Rendering Mermaid diagrams to PNG…")
n = replace_mermaid_blocks(nb)
print(f" replaced {n} mermaid blocks with image references")
NB_PATH.write_text(json.dumps(nb, indent=1))
if not args.skip_execute:
print("\n[2/2] Executing preview cells to bake static outputs…")
execute_preview_cells(NB_PATH)
print(f"\nDone. {NB_PATH}")
print(f" {DIAGRAMS_DIR} ({sum(1 for _ in DIAGRAMS_DIR.glob('*.png'))} PNGs)")
if __name__ == "__main__":
main()
|