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)
```