"""Static, self-contained HTML export — the shareable artifact. Two pages, both built from the same `tuning.render` panels the Gradio app uses: - `audit_html(result, recipe, ...)` step-by-step audit of ONE run (like the old scripts/audit_pipeline.py, but for any recipe), optionally with the gold IoU. - `leaderboard_html(entries)` a recipe-vs-recipe metrics table to pick a winner. All images are inlined as data-URI JPEGs, so a single .html file is the deliverable. """ from __future__ import annotations from lawn_estimator.segmentation import ( CASCADE_PRIMARY_MODEL_ID, LAWN_MODEL_ID, _predict_classes, ) from tuning import render as R _STYLE = """ :root{--bg:#0f130d;--card:#181d14;--ink:#e6ebe0;--soft:#9fb090;--line:#2c3626;--acc:#5db85d} *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 "Avenir Next",system-ui,sans-serif} main{max-width:960px;margin:0 auto;padding:24px 16px 64px} h1{font-size:clamp(22px,4vw,30px);margin:.2em 0} .lead{color:var(--soft);margin:0 0 8px} .final{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--acc); border-radius:10px;padding:12px 16px;margin:14px 0 6px;font-size:18px} .final b{color:var(--acc);font-size:22px} .step{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;margin:18px 0} .hd{display:flex;align-items:center;gap:10px} .hd .n{background:var(--acc);color:#06210a;font-weight:700;width:28px;height:28px;border-radius:50%; display:flex;align-items:center;justify-content:center;flex:none} .hd h2{font-size:17px;margin:0;flex:1}.hd .sq{color:var(--acc);font-weight:600;font-variant-numeric:tabular-nums} .dsc{color:var(--soft);font-size:14px;margin:8px 2px 10px} .step img{width:100%;border-radius:8px;display:block} .legend{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} .chip{display:flex;align-items:center;gap:6px;background:#0f130d;border:1px solid var(--line); border-radius:7px;padding:4px 9px;font-size:12.5px} .chip .sw{width:13px;height:13px;border-radius:3px;border:1px solid #0006} .tblwrap{overflow-x:auto;margin:18px 0} table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums} th,td{border:1px solid var(--line);padding:9px 11px;text-align:right} thead th{background:#20281a;color:var(--soft)}th.l,td.l{text-align:left} tr.best td{background:#1c2a17} """ def _legend_html(legend) -> str: if not legend: return "" chips = "".join( f'' f'{e["name"]} {e["pct"]}%' for e in legend) return f'
{chips}
' def _step(n, title, desc, sqft, img, legend="") -> str: return (f'
{n}' f'

{title}

{sqft}
' f'

{desc}

{title}{legend}
') def _segmentation_steps(cap, recipe, start_n) -> list[str]: """Per-class panels appropriate to the recipe's segmenter (or its fusion base).""" img = cap["image"] steps, n = [], start_n # For fusion, show the LEAF-ON base segmenter's read (that's the lawn source). eff = recipe.fusion_base_segmenter if recipe.segmenter == "leafon_leafoff_fusion" else recipe.segmenter if eff == "eomt-cascade": panel, leg = R.class_panel(img, _predict_classes(CASCADE_PRIMARY_MODEL_ID, img), R.ADE_NAMES) steps.append(_step(n, "Segmentation — EoMT-DINOv3 (cascade primary)", "Primary read, real ADE20K class names.", "", R.b64(panel), _legend_html(leg))) n += 1 panel, leg = R.class_panel(img, _predict_classes(LAWN_MODEL_ID, img), R.M2F_NAMES) steps.append(_step(n, "Segmentation — mask2former (arbiter)", "Arbitrates the cascade's sidewalk/earth classes.", "", R.b64(panel), _legend_html(leg))) n += 1 elif eff in ("mask2former", "eomt-solo", "trained"): from tuning.harness import _segmenter_model_id model_id = _segmenter_model_id(recipe) if eff == recipe.segmenter else ( "tue-mps/eomt-dinov3-ade-semantic-large-512" if eff == "eomt-solo" else LAWN_MODEL_ID) names = R.ADE_NAMES if "eomt" in model_id or "ade" in model_id else R.M2F_NAMES panel, leg = R.class_panel(img, _predict_classes(model_id, img), names) steps.append(_step(n, f"Segmentation — {model_id}", "Per-class prediction for the chosen model.", "", R.b64(panel), _legend_html(leg))) n += 1 # color / sam3 have no per-class map to show here. return steps def audit_html(result, recipe, gold_mask=None, gold_meta=None) -> str: """Step-by-step audit page for one recipe run.""" cap = result.capture img = cap["image"] steps = [] steps.append(_step(1, "Analysis imagery", f"{recipe.imagery} tile, {img.size[0]}×{img.size[1]} px — every measurement runs on this.", "", R.b64(img))) steps.append(_step(2, "Legal parcel", f"County parcel boundary (yellow). {cap['parcel_area_sqft']:,.0f} sqft legal lot.", "", R.b64(R.draw_outlines(img, cap["legal_outlines"], (255, 220, 0))))) ext = "; ".join(f"{e.get('street') or '?'} +{e['area_sqft']:,.0f}" for e in cap["extensions"]) geo_img = R.draw_outlines(R.draw_outlines(img, cap["legal_outlines"], (255, 220, 0)), cap["estimation_outlines"], (0, 220, 220)) steps.append(_step(3, "Estimation geometry (to the curb)", f"Parcel extended to street-facing curbs (cyan). {cap['estimation_area_sqft']:,.0f} sqft. " f"Extensions: {ext or 'none'}.", "", R.b64(geo_img))) steps.append(_step(4, "Color-threshold vegetation (baseline)", "Pure HSV/green rule — the sanity baseline, not the estimate.", f"{cap['rgb_veg_sqft']:,.0f} sqft", R.b64(R.overlay(img, cap["veg_in_parcel"], (60, 220, 60))))) n = 5 seg_steps = _segmentation_steps(cap, recipe, n) steps.extend(seg_steps) n += len(seg_steps) if cap.get("leaf_off_image") is not None: # leaf-on/leaf-off fusion loff = cap["leaf_off_image"] steps.append(_step(n, "Leaf-off (DOGIS) — same frame", "The pixel-registered leaf-off ortho: bare branches expose the ground under canopy.", "", R.b64(loff))) n += 1 steps.append(_step(n, f"Carved hardscape ({recipe.fusion_hardscape_detector})", "Hardscape found on the leaf-off tile under the canopy (red) — SUBTRACTED from lawn. " "This is the hidden driveway/patio the canopy flag only warns about.", "", R.b64(R.overlay(loff, cap["fusion_carve"], (235, 60, 60))))) n += 1 steps.append(_step(n, "Lawn-area mask (candidate lawn)", "Segmenter decision (+ any reclaim) — the pixels eligible as lawn.", "", R.b64(R.overlay(img, cap["lawn_area_in_parcel"], (60, 220, 60))))) n += 1 if cap.get("not_lawn_mask") is not None: steps.append(_step(n, f"Restrict — {recipe.restrict} not-lawn mask", "The roof/driveway/sidewalk (red) this restrict tool removes from the " "LiDAR count. Compare SAM1 vs SAM3 here — same lot, different recipe.", "", R.b64(R.overlay(img, cap["not_lawn_mask"], (235, 60, 60))))) n += 1 if cap["est"].viz.get("ground_px") is not None: steps.append(_step(n, "LiDAR ground points → classified", "Ground returns colored: green = counted as lawn, red = removed (hardscape/roof, " "incl. restrict). sqft = green / all-ground × ground-sampled area.", f"{result.lawn_sqft:,.0f} sqft", R.b64(R.lidar_panel(img, cap["est"].viz)))) n += 1 if gold_mask is not None: scored = "" from tuning.metrics import mask_scores sc = mask_scores(cap["lawn_area_in_parcel"], gold_mask) true_sqft = (gold_meta or {}).get("mask_sqft") if true_sqft: err = (result.lawn_sqft - true_sqft) / true_sqft * 100 scored = f" · true {true_sqft:,.0f} sqft, error {err:+.1f}%" steps.append(_step(n, "vs. gold (hand-drawn mowable area)", f"Green = agree, red = recipe over-counts, blue = recipe misses. " f"IoU {sc['iou']}, precision {sc['precision']}, recall {sc['recall']}{scored}.", "", R.b64(R.iou_overlay(img, cap["lawn_area_in_parcel"], gold_mask)))) final = (f'
Measured lawn: {result.lawn_sqft:,.0f} sqft · ' f'recipe: {recipe.name} · method: {result.method} ({result.confidence})
') body = f'

Pipeline audit

{result.address}

{final}' + "".join(steps) return f'Pipeline audit — {result.address}
{body}
' def leaderboard_html(entries: list[dict]) -> str: """`entries` = [{name, summary(dict from metrics.aggregate)}], best MAE highlighted.""" ranked = sorted([e for e in entries if e["summary"].get("n")], key=lambda e: e["summary"]["mae"]) cols = [("mae", "MAE sqft"), ("mape_pct", "MAPE %"), ("median_ape_pct", "median APE %"), ("bias_pct", "bias %"), ("rmse", "RMSE"), ("r2", "R²"), ("mean_iou", "mean IoU"), ("n", "lots")] head = "".join(f"{lbl}" for _, lbl in cols) rows = "" for i, e in enumerate(ranked): s = e["summary"] cells = "".join(f"{'' if s.get(k) is None else s.get(k)}" for k, _ in cols) rows += f'{e["name"]}{cells}' body = (f'

Recipe leaderboard

' f'

Ranked by MAE against the gold set — lowest error at top.

' f'
{head}' f'{rows}
recipe
') return f'Recipe leaderboard
{body}
'