car-crash-fix-amount-predictor / scripts /render_notebook_assets.py
theDocWho
Pre-render Mermaid diagrams + bake preview outputs into submission notebook (#28)
6e031e4 unverified
Raw
History Blame Contribute Delete
8.82 kB
"""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 ![](image)
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"![diagram](assets/diagrams/{seen[body]})"
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"![diagram](assets/diagrams/{name})"
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()