Spaces:
Running
Running
File size: 10,781 Bytes
a008656 bd90480 707cb8d c837844 9d1c259 0ae5321 9d1c259 bd90480 c837844 bd90480 0ae5321 7ee8be4 0ae5321 568eb4a 0ae5321 bd90480 0ae5321 bd90480 107ba74 7386a5b 107ba74 9d1c259 875d804 bd90480 9d1c259 107ba74 9d1c259 bd90480 9d1c259 bd90480 0ae5321 031eb10 9d1c259 bd90480 031eb10 9d1c259 bd90480 9d1c259 031eb10 bd90480 9d1c259 bd90480 9d1c259 bd90480 9d1c259 c837844 9d1c259 bd90480 9d1c259 c837844 9d1c259 a43df8c 17bb3ac a43df8c bd90480 9d1c259 774b89e c837844 9d1c259 bd90480 9d1c259 c837844 bd90480 c837844 bd90480 | 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 | ---
title: Lawn Estimator
emoji: 🌱
colorFrom: green
colorTo: gray
sdk: docker
app_port: 8000
pinned: false
---
# Lawn Estimator
A **multi-tenant lawn-care SaaS** for the **Omaha, NE metro** (**Douglas + Sarpy counties**),
built on a lawn-**measurement engine**. From an address it measures the square footage a crew
would actually mow — LiDAR ground returns + satellite-image segmentation, including the mowed
right-of-way strip between the property line and the curb — then prices it per the business's
config.
It ships as an embeddable **quote widget** (homeowner: address → instant priced quote →
adjustable lawn outline → book), an **operator dashboard** (usage, leads, batch quoting, config
editor, team, a measurement popup with drag-to-edit + revertible history), and an internal
**/admin** console. Multi-tenant, **Clerk** auth, **Neon**-backed. **Live** at
https://tempuraml-lawn-estimator.hf.space (migrating to **GCP Cloud Run**).
**Runs on free data sources at $0** — Google Maps is an optional accuracy upgrade.
| Input | `"5534 Mayberry St, Omaha, NE 68106"` |
|---|---|
| Output | lawn sqft · legal parcel · to-curb area · method + confidence · per-frontage extensions · a priced quote · a 4-panel visualization |
## How it works
1. **Region + geocode** — county Address_Points first (free, keyless, building
points), auto-selecting Douglas or Sarpy; validated Google fallback. Out-of-area
addresses fail with a clear message (never a wrong estimate).
2. **Analysis imagery** — `auto`: current leaf-on Google (when a billed key exists;
handles new construction) else leaf-on NAIP. Visualizations render over this
*same* leaf-on analysis image, so a correct measurement never looks wrong on a
stale/leaf-off basemap (the county-ortho display adapter is retained but unused).
3. **Parcel** — point-in-polygon query against the region's parcel layer.
4. **Street-aware right-of-way** — extend each parcel edge to the curb, but ONLY on
edges that face a street (via county street centerlines), then subtract
neighboring parcels. Corner lots get both frontages; backyard/field/neighbor
edges get none. Each frontage's added area is reported separately.
5. **RGB segmentation** — marks where lawn could be (open ground + canopy; excludes
buildings, driveways, street pavement). Prod runs a cascade: EoMT-DINOv3 reads the
tile, `mfaytin/mask2former-satellite` arbitrates its weak classes. Accuracy is an
active workstream (a proprietary model trained on our own county imagery is in
progress — see [docs/model-upgrade-plan.md](docs/model-upgrade-plan.md)).
6. **Estimation strategy** — LiDAR ground points × RGB mask (high confidence);
parcels with no usable LiDAR fall back to imagery-only (medium confidence).
The sqft multiplier is the LiDAR-**sampled** area, so buildings/pools (no ground
returns) aren't redistributed onto the lawn.
Why both sensors: RGB alone can't see lawn under trees; LiDAR alone can't tell lawn
from driveway. Each covers the other's blind spot — and imagery-only is the graceful
fallback where LiDAR is absent. Full architecture in [CLAUDE.md](CLAUDE.md); adding a
county in [docs/adding-a-region.md](docs/adding-a-region.md).
## Setup
Python 3.11+, plain pip — every dependency ships wheels (LAS files are read
with laspy; no PDAL/GDAL system libraries needed):
```bash
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e ".[api]"
```
For a **reproducible / server** install (what CI and the Docker image use), install
the hash-pinned runtime deps from the lockfile instead of the abstract extras:
```bash
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cpu
pip install --require-hashes -r requirements-api.lock
pip install --no-deps -e .
```
`requirements-api.txt` is the human-edited source of intent; regenerate the lock after
editing it with
`uv pip compile requirements-api.txt -o requirements-api.lock --python-version 3.11 --python-platform x86_64-unknown-linux-gnu --generate-hashes`.
The segmentation/SAM model revisions are pinned in code (`segmentation.MODEL_REVISIONS`,
`restrict.SAM_REVISION`); bump them deliberately and re-run the Douglas regression gate.
Copy `.env.example` to `.env` (everything is optional for the free path).
> **Note for the original dev machine:** the repo's legacy `conda-env/` still
> works but must be entered via `conda run -p ./conda-env` — invoking its
> python.exe directly leaves conda's DLL directory off PATH and numpy's LAPACK
> hard-crashes with no traceback.
## Usage
```bash
# Single address (imagery auto: Google if keyed, else NAIP)
lawn-estimator --address "14052 Hartman Ave, Omaha, NE 68164"
# Batch (CSV needs an 'Address' column)
lawn-estimator --csv data/lawn_care_schedule.csv
# Also works: python lawn_lidar.py ... / python -m lawn_estimator.cli ...
```
Results: console table, per-address PNG in `data/outputs/{county|naip|google}/`, and a
results CSV for batches. First run downloads the LiDAR tile index (~40 MB) and the
tiles covering your addresses (~100-200 MB each, cached in `data/lidar/`).
Debug the segmentation classes for an address:
```bash
python scripts/debug_hardscape.py --address "17531 Madison St, Omaha, NE 68135"
```
## API + web surfaces
```bash
ALLOWED_API_KEYS=<secret> uvicorn lawn_estimator.api:app --host 0.0.0.0 --port 8000
```
One FastAPI app serves everything:
- **`GET /`** — the partner page. **`GET /widget`** — the embeddable homeowner quote widget.
**`GET /dashboard`** — the operator dashboard (Clerk sign-in). **`GET /admin`** — internal
platform console.
- **`POST /quote`** — single address (20-60 s; ~14 s warm). **`POST /quote/batch`** → `{job_id}`,
poll **`GET /jobs/{job_id}`** — batches are **durable** background jobs (persist across
restarts; cap 50). **`POST /book`** / **`POST /lead`** — widget checkout + lead capture.
- **`/dashboard/*`** — usage, leads, members (team), config draft→publish, batch, measurement
edit + history, embed snippet, tenant-scoped viz.
- **`GET /viz/{id}.png`** / **`/overlay/{id}.png`** — the 4-panel viz / customer lawn overlay.
Interactive API docs at **`/docs`**.
API endpoints use `X-API-Key` (comma-separated `ALLOWED_API_KEYS`; widget keys are publishable +
domain-locked); the dashboard uses **Clerk** sessions (or a `DASHBOARD_DEV_TOKEN` shim locally).
Each quote carries a `pricing` breakdown from the config-driven engine (`pricing.py`, per-tenant
`pricing_configs/`). Product roadmap: [ROADMAP_APP.md](ROADMAP_APP.md); architecture:
[CLAUDE.md](CLAUDE.md). **To browser-test the dashboard:** `python scripts/dev_seed.py` then
`python scripts/browser_smoke.py` (Playwright; no Node needed).
## Development
Install the dev tools (`pytest`, `ruff`, `httpx`) and run the checks:
```bash
pip install -e ".[api,dev]"
make lint # ruff check src tests
make test # pytest — fast (<30 s), zero-network
```
On the original conda machine, prefix with the conda runner:
```bash
CONDA="c:/Users/sergi/miniconda3/Scripts/conda.exe run -p ./conda-env --no-capture-output"
$CONDA ruff check src tests
$CONDA python -m pytest
# or, via the Makefile: make test RUN="$CONDA"
```
`tests/` encodes the cases verified by hand — the wrong-house geocode guard, both
county parcel schemas, corner-lot / deep-lot right-of-way geometry, the LiDAR→RGB
fallback, and the API contract — all mocked, so no network or model download is
needed (an autouse guard fails any test that opens a real socket).
**CI** (`.github/workflows/ci.yml`) runs lint + tests on every PR and on pushes to
`dev`/`main`. When touching the pipeline, keep the byte-identical Douglas QA gate green:
```bash
python scripts/check_regression.py # exits non-zero on any numeric drift
python scripts/check_regression.py --strict # also fail if an address couldn't be measured
```
It clears every measurement flag first (a stray `.env` would otherwise invalidate the run),
records the effective flag state in a `manifest.json`, and separates real drift from an
upstream outage. The gate needs network + models, so it is **not** part of the CI suite —
run it locally when you touch the engine.
## Deployment (free)
**Already live** on Hugging Face Spaces (Docker SDK, free CPU tier). Deployment is
**automated** via GitHub Actions (`.github/workflows/deploy.yml`): a push to `dev`
deploys the staging Space (`TempuraML/lawn-estimator-dev`), a push to `main` deploys
prod (`TempuraML/lawn-estimator`). Auth is a GitHub secret `HF_TOKEN` + variable
`HF_USERNAME`; a manual `git push` to the `space` remote still works as a fallback.
The Dockerfile is pip-only (`python:3.11-slim`, laspy for LAZ, CPU torch, model
pre-baked). To stand up a new Space: create a Docker Space with `app_port: 8000`, add
secrets `ALLOWED_API_KEYS` + Google keys, push, smoke-test `/health`.
Notes: the Space sleeps after ~48 h idle (a daily keep-warm workflow pings `/health`); disk is
ephemeral, so LiDAR tiles re-download after restarts (free, just slower — an optional Cloudflare
R2 tile cache removes this once configured). **Batch jobs now persist** in the DB and resume
after a restart, so a redeploy no longer loses in-flight work.
**Migrating to GCP Cloud Run** (infra provisioned): `.github/workflows/deploy-cloudrun.yml`
builds + deploys on manual dispatch; the app stays served by FastAPI and Neon (DB) is portable
via `DATABASE_URL`. Runbook: [docs/infra-cloud-run.md](docs/infra-cloud-run.md).
## Repository layout
Each folder has its own **`CLAUDE.md`** with the conventions for working in it. Top level:
```
src/lawn_estimator/ engine: pipeline · geometry · segmentation · strategies · regions ·
viz · sources/ (ports+adapters). app: api · db · tenants · metering ·
pricing · dashboard · clerk · tasks · observability · web/ (widget,
dashboard, admin pages)
scripts/ dev_seed.py + browser_smoke.py (Playwright dashboard test) ·
debug_hardscape · run_eval · run_ablation · audit_pipeline
tests/ pytest suite (zero-network) · conftest fakes
docs/ infra-cloud-run.md · adding-a-region.md · product-design-plan.md ·
model-upgrade-plan.md · accuracy-experiments.md · data-model/*.dbml
data/ regression/ (byte-identical gate) · evals/ · lidar/ (cache) · outputs/
CLAUDE.md architecture + design decisions + status (agent handoff)
ROADMAP.md (engine) · ROADMAP_APP.md (product)
```
|