Spaces:
Runtime error
Pre-render Mermaid diagrams + bake preview outputs into submission notebook (#28)
Browse filesThe notebook on GitHub PR view was showing Mermaid blocks as raw code
(GitHub doesn't render Mermaid inside .ipynb cells) and the dataset/inference
preview cells were blank (no saved outputs). Both fixed.
New: scripts/render_notebook_assets.py
Two-pass renderer for ccdp_submission.ipynb:
1. Mermaid → PNG. Each ```mermaid block in markdown cells is pushed to
mermaid.ink, the PNG fetched and saved to notebooks/assets/diagrams/,
and the markdown rewritten to use .
11 diagrams rendered (39–63 KB each).
2. Cell execution. nbclient runs the notebook with allow_errors=True,
stubbing only the two runtime-only cells (Colab upload widget +
inference-on-IMG_PATH). Outputs saved into the .ipynb so the rendered
view on GitHub now shows:
- §1.5 CarDD val grid (random 8 images, matplotlib subplot)
- §1.5 Stanford-Cars random samples grid
- §3.3 damage_seg predicted-mask overlay (input + masks side by side)
- §2.2 live identifier introspection (text — class count, best_val, sha)
- §1.3 weight-fetch log + 14 other text-output cells
The script is idempotent — rerun any time the notebook changes.
Also updated scripts/build_submission_package.py to copy notebooks/assets/
into the standalone package, so the offline-runnable bundle also renders
diagrams correctly.
Submission notebook ccdp_submission.ipynb included here (originated in
PR #26; this PR consolidates with the asset-rendering work).
Build script scripts/build_submission_package.py also included
(originated in PR #27; consolidated here).
Resulting deliverables:
- notebooks/ccdp_submission.ipynb: 52 cells, 11 inline diagram PNGs,
3 embedded image outputs, 19 cells with saved outputs.
- submission_package/ + ccdp_submission_v0.2.0.zip: 132 MB / 121.8 MB,
95 files including assets/diagrams/.
Co-authored-by: Abhishek Roy <abhishek.ashish.roy@gmail.com>
- notebooks/assets/diagrams/diagram_00_933b0adbc9.png +0 -0
- notebooks/assets/diagrams/diagram_01_6474ec3ab2.png +0 -0
- notebooks/assets/diagrams/diagram_02_0a4c505a29.png +0 -0
- notebooks/assets/diagrams/diagram_03_fcd083b4cb.png +0 -0
- notebooks/assets/diagrams/diagram_04_2af2d350c9.png +0 -0
- notebooks/assets/diagrams/diagram_05_8ed68e5ef8.png +0 -0
- notebooks/assets/diagrams/diagram_06_34526512c0.png +0 -0
- notebooks/assets/diagrams/diagram_07_8ce287c7c7.png +0 -0
- notebooks/assets/diagrams/diagram_08_02d70d657d.png +0 -0
- notebooks/assets/diagrams/diagram_09_7c220ad7a0.png +0 -0
- notebooks/assets/diagrams/diagram_10_f1e42b3487.png +0 -0
- notebooks/ccdp_submission.ipynb +0 -0
- scripts/build_submission_package.py +378 -0
- scripts/render_notebook_assets.py +227 -0
|
|
|
|
|
|
|
|
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the standalone submission package.
|
| 2 |
+
|
| 3 |
+
Produces `submission_package/` (and optionally a zip) containing everything
|
| 4 |
+
needed to run `ccdp_submission.ipynb` without internet, git, or the GitHub
|
| 5 |
+
release — bundles the package source, trained weights, sample images, and
|
| 6 |
+
a self-contained README.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python scripts/build_submission_package.py # build folder only
|
| 10 |
+
python scripts/build_submission_package.py --zip # also build .zip
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import shutil
|
| 17 |
+
import sys
|
| 18 |
+
import zipfile
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 22 |
+
OUT = ROOT / "submission_package"
|
| 23 |
+
PKG_NAME = "ccdp_submission_v0.2.0"
|
| 24 |
+
|
| 25 |
+
# Map of submission-package weight name -> source path on the dev machine.
|
| 26 |
+
# `identifier.pt` is the NEW VMMRdb-trained one (101 MB) from the Colab run;
|
| 27 |
+
# the production/ identifier predates that and is the Stanford-only version.
|
| 28 |
+
WEIGHT_SOURCES = {
|
| 29 |
+
"identifier.pt": ROOT / "checkpoints/identifier/identifier.pt",
|
| 30 |
+
"damage_seg.pt": ROOT / "checkpoints/production/yoloseg.pt",
|
| 31 |
+
"parts_seg.pt": ROOT / "checkpoints/production/parts.pt",
|
| 32 |
+
"damage_det.pt": ROOT / "checkpoints/production/detector.pt",
|
| 33 |
+
# damage_cls.pt (Variant A) is 283 MB — skipped to keep the zip portal-friendly.
|
| 34 |
+
# The notebook's Variant A cell handles the missing-weight case gracefully.
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# CarDD val images we ship as the demo inputs.
|
| 38 |
+
SAMPLE_IMAGE_DIR = ROOT / "data/raw/car-damage-detection/CarDD_release/CarDD_COCO/val2017"
|
| 39 |
+
SAMPLE_IMAGE_COUNT = 10
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
REQUIREMENTS_TXT = """\
|
| 43 |
+
# Install: pip install -r requirements.txt
|
| 44 |
+
# Then: pip install -e . (from this folder) so `import ccdp` works.
|
| 45 |
+
|
| 46 |
+
torch>=2.2
|
| 47 |
+
torchvision>=0.17
|
| 48 |
+
ultralytics>=8.1
|
| 49 |
+
xgboost>=2.0
|
| 50 |
+
scikit-learn>=1.4
|
| 51 |
+
pandas>=2.2
|
| 52 |
+
numpy>=1.26
|
| 53 |
+
pillow>=10.2
|
| 54 |
+
opencv-python-headless>=4.9
|
| 55 |
+
matplotlib>=3.8
|
| 56 |
+
pyyaml>=6.0
|
| 57 |
+
requests>=2.31
|
| 58 |
+
typer>=0.12
|
| 59 |
+
rich>=13.7
|
| 60 |
+
pydantic>=2.6
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
README_MD = f"""\
|
| 65 |
+
# CCDP — Car Crash Fix-Amount Predictor (capstone submission package)
|
| 66 |
+
|
| 67 |
+
Standalone reproducible bundle of the project. **No internet, git, or
|
| 68 |
+
GitHub-release download required** — code, trained weights, and sample
|
| 69 |
+
images are all in this folder.
|
| 70 |
+
|
| 71 |
+
## What's in here
|
| 72 |
+
|
| 73 |
+
```
|
| 74 |
+
{PKG_NAME}/
|
| 75 |
+
├── README.md # this file
|
| 76 |
+
├── ccdp_submission.ipynb # the single-notebook submission
|
| 77 |
+
├── requirements.txt # pip deps
|
| 78 |
+
├── pyproject.toml # package metadata so `pip install -e .` works
|
| 79 |
+
├── CITATIONS.md # dataset citations
|
| 80 |
+
├── src/ccdp/ # the Python package (vendored)
|
| 81 |
+
├── models/ # 4 trained model weights (~120 MB)
|
| 82 |
+
│ ├── identifier.pt # ResNet-50 make/model identifier (VMMRdb 1163-class)
|
| 83 |
+
│ ├── damage_seg.pt # YOLOv8-seg damage masks (CarDD nc=6)
|
| 84 |
+
│ ├── parts_seg.pt # YOLOv8-seg car-parts masks (nc=15)
|
| 85 |
+
│ └── damage_det.pt # YOLOv8 damage box detector (Variant B)
|
| 86 |
+
└── sample_images/ # {SAMPLE_IMAGE_COUNT} CarDD val images for the demo
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## How to run
|
| 90 |
+
|
| 91 |
+
### Option A — Locally (recommended for review)
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# 1. Create a fresh Python 3.10+ venv
|
| 95 |
+
python -m venv .venv
|
| 96 |
+
source .venv/bin/activate
|
| 97 |
+
|
| 98 |
+
# 2. Install deps + the bundled package
|
| 99 |
+
pip install -r requirements.txt
|
| 100 |
+
pip install -e .
|
| 101 |
+
|
| 102 |
+
# 3. Launch Jupyter
|
| 103 |
+
pip install jupyter
|
| 104 |
+
jupyter lab # or: jupyter notebook
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
Open `ccdp_submission.ipynb` and run all cells top-to-bottom. **No training
|
| 108 |
+
is required to see results** — every training cell is guarded by
|
| 109 |
+
`RUN_TRAINING = False`, and the demo uses the bundled weights in `models/`.
|
| 110 |
+
|
| 111 |
+
### Option B — Google Colab
|
| 112 |
+
|
| 113 |
+
1. Zip this folder, upload to Google Drive.
|
| 114 |
+
2. Open a new Colab notebook, run:
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
from google.colab import drive; drive.mount('/content/drive')
|
| 118 |
+
!unzip -q /content/drive/MyDrive/{PKG_NAME}.zip -d /content/
|
| 119 |
+
%cd /content/{PKG_NAME}
|
| 120 |
+
!pip -q install -r requirements.txt
|
| 121 |
+
!pip -q install -e .
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
3. Open `ccdp_submission.ipynb` from the file browser and run.
|
| 125 |
+
|
| 126 |
+
The notebook auto-detects Colab vs. local and adjusts paths.
|
| 127 |
+
|
| 128 |
+
## What the notebook does
|
| 129 |
+
|
| 130 |
+
1. **§1** Sanity-check the environment and copy bundled weights into the
|
| 131 |
+
expected `checkpoints/production/` path.
|
| 132 |
+
2. **§1.4** Datasets used + citations.
|
| 133 |
+
3. **§1.5** Live preview of sample images.
|
| 134 |
+
4. **§2** Identifier training pipeline + final v0.2.0 metrics (1163-class
|
| 135 |
+
val acc 0.3304, Stanford make-anchor 0.163).
|
| 136 |
+
5. **§3** Damage segmentation training (CarDD nc=6).
|
| 137 |
+
6. **§3b** (optional) Path A extension with HITL.
|
| 138 |
+
7. **§4** Parts segmentation training (carparts nc=15).
|
| 139 |
+
8. **§5–§8** Variant A → B → C → D walkthrough (the core methodology).
|
| 140 |
+
9. **§9** Multi-car extension.
|
| 141 |
+
10. **§10** Live inference demo on sample images.
|
| 142 |
+
11. **§11** Reproducibility checklist + final metrics table.
|
| 143 |
+
|
| 144 |
+
## What if I want to re-train?
|
| 145 |
+
|
| 146 |
+
Every training cell is guarded:
|
| 147 |
+
|
| 148 |
+
```python
|
| 149 |
+
RUN_TRAINING = False
|
| 150 |
+
# Production values: epochs=80, batch=16, imgsz=640, patience=20
|
| 151 |
+
SMOKE = dict(epochs=1, batch=2, imgsz=320, patience=5)
|
| 152 |
+
if RUN_TRAINING:
|
| 153 |
+
... # uses smoke values by default; substitute production for real runs
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
Flip `RUN_TRAINING = True` and swap in production values when on a GPU.
|
| 157 |
+
|
| 158 |
+
## Notes for the reviewer
|
| 159 |
+
|
| 160 |
+
- `models/identifier.pt` is **self-describing** — `class_names`, `num_classes`,
|
| 161 |
+
`best_val`, and the training config are embedded in the .pt itself.
|
| 162 |
+
§2.2 of the notebook loads and prints them as the live model card.
|
| 163 |
+
- The `damage_cls.pt` (Variant A multilabel head) is NOT included — it's
|
| 164 |
+
283 MB and Variant A is shown only schematically. Variant D is what
|
| 165 |
+
ships in production.
|
| 166 |
+
- Datasets cited in `CITATIONS.md` are NOT bundled — see citations for
|
| 167 |
+
the canonical Kaggle / HF source links.
|
| 168 |
+
|
| 169 |
+
## Links
|
| 170 |
+
|
| 171 |
+
- Code repo: <https://github.com/theDocWho/car-crash-fix-amount-predictor>
|
| 172 |
+
- Weights release: v0.2.0 on the same repo
|
| 173 |
+
- Live demo: HuggingFace Space (see repo README)
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# -----------------------------------------------------------------------------
|
| 178 |
+
# Notebook patching: take the canonical notebook and rewrite the setup cells
|
| 179 |
+
# so they work standalone (no git clone, no release download).
|
| 180 |
+
# -----------------------------------------------------------------------------
|
| 181 |
+
|
| 182 |
+
NB_SETUP_INSTALL_CELL = """\
|
| 183 |
+
# === Submission-package setup ===
|
| 184 |
+
# This notebook is shipped inside `{pkg_name}/`. The bundled package source is
|
| 185 |
+
# in `src/ccdp/`, weights in `models/`, sample images in `sample_images/`.
|
| 186 |
+
# Run `pip install -r requirements.txt && pip install -e .` from the package
|
| 187 |
+
# root BEFORE opening this notebook (see README.md).
|
| 188 |
+
|
| 189 |
+
import os, sys, pathlib
|
| 190 |
+
PKG_ROOT = pathlib.Path('.').resolve()
|
| 191 |
+
# Detect Colab so paths still resolve if the user opened the notebook from /content/
|
| 192 |
+
if 'google.colab' in sys.modules and not (PKG_ROOT / 'src' / 'ccdp').exists():
|
| 193 |
+
# Try the conventional unzipped location
|
| 194 |
+
cands = sorted(pathlib.Path('/content').glob('{pkg_name}*'))
|
| 195 |
+
if cands:
|
| 196 |
+
PKG_ROOT = cands[-1].resolve()
|
| 197 |
+
os.chdir(PKG_ROOT)
|
| 198 |
+
print(f'Switched to {{PKG_ROOT}}')
|
| 199 |
+
|
| 200 |
+
assert (PKG_ROOT / 'src' / 'ccdp').exists(), (
|
| 201 |
+
f"Can't find src/ccdp at {{PKG_ROOT}}. Open this notebook from the package root.")
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
import ccdp
|
| 205 |
+
print(f'ccdp imported OK from {{PKG_ROOT}}')
|
| 206 |
+
except ImportError:
|
| 207 |
+
print('ccdp not installed — running: pip install -e .')
|
| 208 |
+
os.system('pip -q install -e .')
|
| 209 |
+
import ccdp
|
| 210 |
+
print('ccdp installed and imported')
|
| 211 |
+
""".replace("{pkg_name}", PKG_NAME)
|
| 212 |
+
|
| 213 |
+
NB_WEIGHTS_CELL = """\
|
| 214 |
+
# === Wire bundled weights into the path the inference cells read from ===
|
| 215 |
+
# Copies models/*.pt -> checkpoints/production/<name>.pt (and yoloseg.pt /
|
| 216 |
+
# parts.pt aliases for the existing inference modules).
|
| 217 |
+
import pathlib, shutil
|
| 218 |
+
|
| 219 |
+
PKG_ROOT = pathlib.Path('.').resolve()
|
| 220 |
+
PROD = PKG_ROOT / 'checkpoints' / 'production'
|
| 221 |
+
PROD.mkdir(parents=True, exist_ok=True)
|
| 222 |
+
|
| 223 |
+
# Submission-package name -> destination filenames in checkpoints/production/.
|
| 224 |
+
# The inference modules read 'identifier.pt', 'yoloseg.pt' (damage seg),
|
| 225 |
+
# 'parts.pt', 'detector.pt'.
|
| 226 |
+
MAPPING = {
|
| 227 |
+
'identifier.pt': ['identifier.pt'],
|
| 228 |
+
'damage_seg.pt': ['yoloseg.pt', 'damage_seg.pt'],
|
| 229 |
+
'parts_seg.pt': ['parts.pt', 'parts_seg.pt'],
|
| 230 |
+
'damage_det.pt': ['detector.pt', 'damage_det.pt'],
|
| 231 |
+
}
|
| 232 |
+
for src_name, dst_names in MAPPING.items():
|
| 233 |
+
src = PKG_ROOT / 'models' / src_name
|
| 234 |
+
if not src.exists():
|
| 235 |
+
print(f' {src_name:18s} NOT in models/ — inference cells may fall back to schematics')
|
| 236 |
+
continue
|
| 237 |
+
for dst_name in dst_names:
|
| 238 |
+
dst = PROD / dst_name
|
| 239 |
+
if not dst.exists():
|
| 240 |
+
shutil.copy(src, dst)
|
| 241 |
+
print(f' {src_name:18s} -> {", ".join(str((PROD / d).relative_to(PKG_ROOT)) for d in dst_names)}')
|
| 242 |
+
|
| 243 |
+
# Initialise the parts-cost catalog
|
| 244 |
+
os.system('ccdp costing init || true')
|
| 245 |
+
"""
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def patch_notebook(src_nb: Path, dst_nb: Path) -> None:
|
| 249 |
+
"""Read the canonical notebook, swap out §1.1 install + §1.3 weight-fetch
|
| 250 |
+
cells for standalone equivalents, and write to dst."""
|
| 251 |
+
nb = json.loads(src_nb.read_text())
|
| 252 |
+
|
| 253 |
+
def join_src(c):
|
| 254 |
+
s = c.get("source", "")
|
| 255 |
+
return s if isinstance(s, str) else "".join(s)
|
| 256 |
+
|
| 257 |
+
for cell in nb["cells"]:
|
| 258 |
+
if cell.get("cell_type") != "code":
|
| 259 |
+
continue
|
| 260 |
+
src = join_src(cell)
|
| 261 |
+
# §1.1 — replace the git-clone install cell
|
| 262 |
+
if "git clone" in src and "car-crash-fix-amount-predictor" in src:
|
| 263 |
+
cell["source"] = NB_SETUP_INSTALL_CELL
|
| 264 |
+
# §1.3 — replace the urllib download-from-release cell
|
| 265 |
+
elif "urllib.request.urlretrieve" in src and "releases/download" in src:
|
| 266 |
+
cell["source"] = NB_WEIGHTS_CELL
|
| 267 |
+
# §1.5 / §10 sample-image fallback — also offer the bundled sample_images dir
|
| 268 |
+
elif "data/raw/car-damage-detection" in src and "cardd_val" in src:
|
| 269 |
+
cell["source"] = src.replace(
|
| 270 |
+
"cardd_val = Path('data/raw/car-damage-detection/CarDD_release/CarDD_COCO/val2017')",
|
| 271 |
+
"cardd_val = (Path('sample_images') if Path('sample_images').exists()\n"
|
| 272 |
+
" else Path('data/raw/car-damage-detection/CarDD_release/CarDD_COCO/val2017'))",
|
| 273 |
+
).replace(
|
| 274 |
+
"cardd_val = pathlib.Path('data/raw/car-damage-detection/CarDD_release/CarDD_COCO/val2017')",
|
| 275 |
+
"cardd_val = (pathlib.Path('sample_images') if pathlib.Path('sample_images').exists()\n"
|
| 276 |
+
" else pathlib.Path('data/raw/car-damage-detection/CarDD_release/CarDD_COCO/val2017'))",
|
| 277 |
+
)
|
| 278 |
+
dst_nb.parent.mkdir(parents=True, exist_ok=True)
|
| 279 |
+
dst_nb.write_text(json.dumps(nb, indent=1))
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# -----------------------------------------------------------------------------
|
| 283 |
+
# Build
|
| 284 |
+
# -----------------------------------------------------------------------------
|
| 285 |
+
|
| 286 |
+
def build(out: Path, with_zip: bool) -> None:
|
| 287 |
+
if out.exists():
|
| 288 |
+
shutil.rmtree(out)
|
| 289 |
+
out.mkdir(parents=True)
|
| 290 |
+
|
| 291 |
+
# 1. Vendor the package source
|
| 292 |
+
src_pkg = out / "src" / "ccdp"
|
| 293 |
+
shutil.copytree(ROOT / "src" / "ccdp", src_pkg,
|
| 294 |
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
| 295 |
+
print(f"vendored src/ccdp -> {src_pkg}")
|
| 296 |
+
|
| 297 |
+
# 2. Minimal pyproject.toml — pip install -e . needs this
|
| 298 |
+
pyproject = (ROOT / "pyproject.toml").read_text()
|
| 299 |
+
# Strip the dev/serve extras — submission only needs core + ml
|
| 300 |
+
(out / "pyproject.toml").write_text(pyproject)
|
| 301 |
+
print("wrote pyproject.toml")
|
| 302 |
+
|
| 303 |
+
# 3. requirements.txt
|
| 304 |
+
(out / "requirements.txt").write_text(REQUIREMENTS_TXT)
|
| 305 |
+
print("wrote requirements.txt")
|
| 306 |
+
|
| 307 |
+
# 4. README.md
|
| 308 |
+
(out / "README.md").write_text(README_MD)
|
| 309 |
+
print("wrote README.md")
|
| 310 |
+
|
| 311 |
+
# 5. CITATIONS.md
|
| 312 |
+
shutil.copy(ROOT / "CITATIONS.md", out / "CITATIONS.md")
|
| 313 |
+
print("copied CITATIONS.md")
|
| 314 |
+
|
| 315 |
+
# 6. Bundled weights
|
| 316 |
+
models_dir = out / "models"
|
| 317 |
+
models_dir.mkdir()
|
| 318 |
+
for name, src in WEIGHT_SOURCES.items():
|
| 319 |
+
if not src.exists():
|
| 320 |
+
print(f" WARN: {src} missing — skipping {name}")
|
| 321 |
+
continue
|
| 322 |
+
shutil.copy(src, models_dir / name)
|
| 323 |
+
size_mb = (models_dir / name).stat().st_size / 1e6
|
| 324 |
+
print(f" models/{name} ({size_mb:.1f} MB)")
|
| 325 |
+
|
| 326 |
+
# 7. Sample images
|
| 327 |
+
samples_dir = out / "sample_images"
|
| 328 |
+
samples_dir.mkdir()
|
| 329 |
+
if SAMPLE_IMAGE_DIR.exists():
|
| 330 |
+
for i, p in enumerate(sorted(SAMPLE_IMAGE_DIR.glob("*.jpg"))[:SAMPLE_IMAGE_COUNT]):
|
| 331 |
+
shutil.copy(p, samples_dir / p.name)
|
| 332 |
+
print(f"copied {len(list(samples_dir.iterdir()))} sample images")
|
| 333 |
+
else:
|
| 334 |
+
print(f" WARN: {SAMPLE_IMAGE_DIR} missing — sample_images/ is empty")
|
| 335 |
+
|
| 336 |
+
# 8. Standalone notebook
|
| 337 |
+
patch_notebook(ROOT / "notebooks" / "ccdp_submission.ipynb",
|
| 338 |
+
out / "ccdp_submission.ipynb")
|
| 339 |
+
print("patched + wrote ccdp_submission.ipynb")
|
| 340 |
+
|
| 341 |
+
# 8b. Pre-rendered diagram PNGs (Mermaid → PNG via mermaid.ink). The
|
| 342 |
+
# notebook's markdown references `assets/diagrams/*.png`; these need to
|
| 343 |
+
# ship alongside the .ipynb for offline rendering.
|
| 344 |
+
diagrams_src = ROOT / "notebooks" / "assets" / "diagrams"
|
| 345 |
+
if diagrams_src.exists():
|
| 346 |
+
diagrams_dst = out / "assets" / "diagrams"
|
| 347 |
+
diagrams_dst.mkdir(parents=True, exist_ok=True)
|
| 348 |
+
for p in diagrams_src.glob("*.png"):
|
| 349 |
+
shutil.copy(p, diagrams_dst / p.name)
|
| 350 |
+
n = sum(1 for _ in diagrams_dst.glob("*.png"))
|
| 351 |
+
print(f"copied {n} pre-rendered diagram PNGs -> assets/diagrams/")
|
| 352 |
+
else:
|
| 353 |
+
print(" WARN: notebooks/assets/diagrams/ missing — Mermaid images won't render offline")
|
| 354 |
+
|
| 355 |
+
total = sum(f.stat().st_size for f in out.rglob("*") if f.is_file())
|
| 356 |
+
print(f"\nPackage: {out} ({total/1e6:.1f} MB across {sum(1 for _ in out.rglob('*') if _.is_file())} files)")
|
| 357 |
+
|
| 358 |
+
if with_zip:
|
| 359 |
+
zip_path = out.parent / f"{PKG_NAME}.zip"
|
| 360 |
+
if zip_path.exists():
|
| 361 |
+
zip_path.unlink()
|
| 362 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
|
| 363 |
+
for f in out.rglob("*"):
|
| 364 |
+
if f.is_file():
|
| 365 |
+
zf.write(f, arcname=Path(PKG_NAME) / f.relative_to(out))
|
| 366 |
+
print(f"zipped -> {zip_path} ({zip_path.stat().st_size/1e6:.1f} MB)")
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def main() -> None:
|
| 370 |
+
ap = argparse.ArgumentParser()
|
| 371 |
+
ap.add_argument("--zip", action="store_true", help="Also produce the .zip alongside the folder.")
|
| 372 |
+
ap.add_argument("--out", type=Path, default=OUT, help=f"Output dir (default: {OUT}).")
|
| 373 |
+
args = ap.parse_args()
|
| 374 |
+
build(args.out, with_zip=args.zip)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
if __name__ == "__main__":
|
| 378 |
+
main()
|
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pre-render the submission notebook so it shows everything on GitHub.
|
| 2 |
+
|
| 3 |
+
GitHub renders .ipynb statically — Mermaid code blocks inside markdown cells
|
| 4 |
+
show as plain code (no diagram), and code cells with no saved outputs look
|
| 5 |
+
blank. This script fixes both:
|
| 6 |
+
|
| 7 |
+
1. **Mermaid → PNG.** Each ```mermaid block is replaced with an 
|
| 8 |
+
reference. PNGs fetched from the public mermaid.ink renderer and saved to
|
| 9 |
+
notebooks/assets/diagrams/.
|
| 10 |
+
2. **Dataset previews + inference previews → executed outputs.** Re-executes
|
| 11 |
+
the cells that produce matplotlib grids / inference visualizations so the
|
| 12 |
+
.ipynb on disk ships with embedded PNG outputs that render on GitHub.
|
| 13 |
+
|
| 14 |
+
Idempotent. Re-run any time the notebook changes.
|
| 15 |
+
|
| 16 |
+
Usage:
|
| 17 |
+
python scripts/render_notebook_assets.py
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import base64
|
| 23 |
+
import hashlib
|
| 24 |
+
import json
|
| 25 |
+
import re
|
| 26 |
+
import sys
|
| 27 |
+
import time
|
| 28 |
+
import ssl
|
| 29 |
+
import urllib.request
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import certifi
|
| 34 |
+
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
| 35 |
+
except ImportError:
|
| 36 |
+
_SSL_CTX = ssl.create_default_context()
|
| 37 |
+
|
| 38 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 39 |
+
NB_PATH = ROOT / "notebooks" / "ccdp_submission.ipynb"
|
| 40 |
+
DIAGRAMS_DIR = ROOT / "notebooks" / "assets" / "diagrams"
|
| 41 |
+
MERMAID_INK = "https://mermaid.ink/img/{b64}?type=png&bgColor=ffffff"
|
| 42 |
+
|
| 43 |
+
# Which code cells to execute and bake outputs for (matched by substring).
|
| 44 |
+
# Heavy / network-dependent cells are skipped — we only execute the cheap
|
| 45 |
+
# previewers that produce useful static images.
|
| 46 |
+
EXECUTE_IF_CONTAINS = (
|
| 47 |
+
"show_grid(samples[:8], 'CarDD val samples",
|
| 48 |
+
"show_grid(samples[:8], 'Stanford-Cars random",
|
| 49 |
+
"label_files = list(yolo_dir.glob", # YOLO label preview (text only, but still)
|
| 50 |
+
"model = YOLO(str(weights))", # damage_seg overlay preview (§3.3)
|
| 51 |
+
"print(f'Parts detected in", # parts_seg detection list (§4.1)
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Cells we never auto-execute (require uploaded image, slow, or interactive)
|
| 55 |
+
NEVER_EXECUTE_IF_CONTAINS = (
|
| 56 |
+
"from google.colab import files",
|
| 57 |
+
"estimate_multi(IMG_PATH)",
|
| 58 |
+
"files.upload()",
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Mermaid → PNG
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
def _mermaid_to_png(source: str, dest: Path) -> bool:
|
| 67 |
+
"""POST the Mermaid source to mermaid.ink and save the PNG. Returns True on success."""
|
| 68 |
+
# mermaid.ink uses url-safe base64 (no padding stripped in their decoder).
|
| 69 |
+
b64 = base64.urlsafe_b64encode(source.encode("utf-8")).decode("ascii")
|
| 70 |
+
url = MERMAID_INK.format(b64=b64)
|
| 71 |
+
try:
|
| 72 |
+
req = urllib.request.Request(url, headers={"User-Agent": "ccdp-build/0.1"})
|
| 73 |
+
with urllib.request.urlopen(req, timeout=30, context=_SSL_CTX) as r:
|
| 74 |
+
data = r.read()
|
| 75 |
+
if not data or len(data) < 200:
|
| 76 |
+
print(f" ! mermaid.ink returned {len(data)} bytes for {dest.name} — skipping")
|
| 77 |
+
return False
|
| 78 |
+
dest.write_bytes(data)
|
| 79 |
+
return True
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f" ! mermaid.ink failed for {dest.name}: {e}")
|
| 82 |
+
return False
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
_MERMAID_BLOCK = re.compile(r"```mermaid\n(.*?)```", re.DOTALL)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _stable_id(source: str, ix: int) -> str:
|
| 89 |
+
"""Deterministic name per diagram so re-runs don't churn the disk."""
|
| 90 |
+
h = hashlib.sha1(source.encode()).hexdigest()[:10]
|
| 91 |
+
return f"diagram_{ix:02d}_{h}"
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def replace_mermaid_blocks(nb: dict) -> int:
|
| 95 |
+
"""Mutate notebook in place: replace each Mermaid fence with an image
|
| 96 |
+
reference (relative to the notebook). Return the count replaced."""
|
| 97 |
+
DIAGRAMS_DIR.mkdir(parents=True, exist_ok=True)
|
| 98 |
+
seen: dict[str, str] = {} # source -> rel path (dedupe identical diagrams)
|
| 99 |
+
ix = 0
|
| 100 |
+
n_replaced = 0
|
| 101 |
+
for cell in nb["cells"]:
|
| 102 |
+
if cell.get("cell_type") != "markdown":
|
| 103 |
+
continue
|
| 104 |
+
src = cell["source"] if isinstance(cell["source"], str) else "".join(cell["source"])
|
| 105 |
+
if "```mermaid" not in src:
|
| 106 |
+
continue
|
| 107 |
+
|
| 108 |
+
def _sub(match):
|
| 109 |
+
nonlocal ix, n_replaced
|
| 110 |
+
body = match.group(1).strip()
|
| 111 |
+
if body in seen:
|
| 112 |
+
return f""
|
| 113 |
+
name = _stable_id(body, ix) + ".png"
|
| 114 |
+
ix += 1
|
| 115 |
+
dest = DIAGRAMS_DIR / name
|
| 116 |
+
if not dest.exists():
|
| 117 |
+
ok = _mermaid_to_png(body, dest)
|
| 118 |
+
if not ok:
|
| 119 |
+
return match.group(0) # keep raw fence on failure
|
| 120 |
+
print(f" rendered {name} ({dest.stat().st_size/1024:.0f} KB)")
|
| 121 |
+
time.sleep(0.4) # be polite to mermaid.ink
|
| 122 |
+
else:
|
| 123 |
+
print(f" reuse {name}")
|
| 124 |
+
seen[body] = name
|
| 125 |
+
n_replaced += 1
|
| 126 |
+
return f""
|
| 127 |
+
|
| 128 |
+
new_src = _MERMAID_BLOCK.sub(_sub, src)
|
| 129 |
+
cell["source"] = new_src
|
| 130 |
+
return n_replaced
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ---------------------------------------------------------------------------
|
| 134 |
+
# Executing selected cells to bake outputs
|
| 135 |
+
# ---------------------------------------------------------------------------
|
| 136 |
+
|
| 137 |
+
def _cell_should_execute(src: str) -> bool:
|
| 138 |
+
if any(s in src for s in NEVER_EXECUTE_IF_CONTAINS):
|
| 139 |
+
return False
|
| 140 |
+
return any(s in src for s in EXECUTE_IF_CONTAINS)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def execute_preview_cells(nb_path: Path) -> int:
|
| 144 |
+
"""Execute the notebook full-through (allow_errors=True), then save with
|
| 145 |
+
embedded outputs. Cells that we know will fail (Colab uploads, the
|
| 146 |
+
git-clone install) are temporarily blanked to no-ops so they don't poison
|
| 147 |
+
state for the cells we DO want outputs from."""
|
| 148 |
+
try:
|
| 149 |
+
import nbformat
|
| 150 |
+
from nbclient import NotebookClient
|
| 151 |
+
except ImportError:
|
| 152 |
+
print(" ! nbclient/nbformat not installed — skipping cell execution")
|
| 153 |
+
print(" pip install nbclient nbformat ipykernel")
|
| 154 |
+
return 0
|
| 155 |
+
|
| 156 |
+
nb = nbformat.read(str(nb_path), as_version=4)
|
| 157 |
+
|
| 158 |
+
# Stub out cells that don't make sense in a non-Colab batch context.
|
| 159 |
+
n_stubbed = 0
|
| 160 |
+
for cell in nb.cells:
|
| 161 |
+
if cell.cell_type != "code":
|
| 162 |
+
continue
|
| 163 |
+
src = cell.source if isinstance(cell.source, str) else "".join(cell.source)
|
| 164 |
+
# The §1.1 install cell tries `pip install -e .` which we already did
|
| 165 |
+
# in the dev venv; let it run (it's a noop).
|
| 166 |
+
# Stub the upload widget + the download-from-release cell (slow on a
|
| 167 |
+
# cold machine; release weights already fetched if you ran §1.3 once).
|
| 168 |
+
if any(s in src for s in NEVER_EXECUTE_IF_CONTAINS):
|
| 169 |
+
cell.metadata.setdefault("ccdp", {})["original_source"] = cell.source
|
| 170 |
+
cell.source = "# (skipped during pre-render — runtime-only cell)"
|
| 171 |
+
n_stubbed += 1
|
| 172 |
+
|
| 173 |
+
print(f" stubbed {n_stubbed} runtime-only cells; executing the rest…")
|
| 174 |
+
client = NotebookClient(
|
| 175 |
+
nb, timeout=300, kernel_name="ccdp-dev",
|
| 176 |
+
resources={"metadata": {"path": str(ROOT)}},
|
| 177 |
+
allow_errors=True,
|
| 178 |
+
)
|
| 179 |
+
try:
|
| 180 |
+
client.execute()
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f" ! execution error: {e}")
|
| 183 |
+
|
| 184 |
+
# Restore stubbed sources
|
| 185 |
+
for cell in nb.cells:
|
| 186 |
+
if cell.cell_type == "code" and "ccdp" in cell.metadata \
|
| 187 |
+
and "original_source" in cell.metadata["ccdp"]:
|
| 188 |
+
cell.source = cell.metadata["ccdp"].pop("original_source")
|
| 189 |
+
if not cell.metadata["ccdp"]:
|
| 190 |
+
cell.metadata.pop("ccdp")
|
| 191 |
+
cell.outputs = [] # don't show stub output
|
| 192 |
+
|
| 193 |
+
nbformat.write(nb, str(nb_path))
|
| 194 |
+
n_with_out = sum(1 for c in nb.cells if c.cell_type == "code" and c.get("outputs"))
|
| 195 |
+
print(f" cells with saved outputs: {n_with_out}")
|
| 196 |
+
return n_with_out
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# ---------------------------------------------------------------------------
|
| 200 |
+
# Main
|
| 201 |
+
# ---------------------------------------------------------------------------
|
| 202 |
+
|
| 203 |
+
def main() -> None:
|
| 204 |
+
ap = argparse.ArgumentParser()
|
| 205 |
+
ap.add_argument("--skip-diagrams", action="store_true", help="Skip Mermaid rendering")
|
| 206 |
+
ap.add_argument("--skip-execute", action="store_true", help="Skip cell execution")
|
| 207 |
+
args = ap.parse_args()
|
| 208 |
+
|
| 209 |
+
print(f"Reading {NB_PATH}")
|
| 210 |
+
nb = json.loads(NB_PATH.read_text())
|
| 211 |
+
|
| 212 |
+
if not args.skip_diagrams:
|
| 213 |
+
print("\n[1/2] Rendering Mermaid diagrams to PNG…")
|
| 214 |
+
n = replace_mermaid_blocks(nb)
|
| 215 |
+
print(f" replaced {n} mermaid blocks with image references")
|
| 216 |
+
NB_PATH.write_text(json.dumps(nb, indent=1))
|
| 217 |
+
|
| 218 |
+
if not args.skip_execute:
|
| 219 |
+
print("\n[2/2] Executing preview cells to bake static outputs…")
|
| 220 |
+
execute_preview_cells(NB_PATH)
|
| 221 |
+
|
| 222 |
+
print(f"\nDone. {NB_PATH}")
|
| 223 |
+
print(f" {DIAGRAMS_DIR} ({sum(1 for _ in DIAGRAMS_DIR.glob('*.png'))} PNGs)")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
if __name__ == "__main__":
|
| 227 |
+
main()
|