Spaces:
Runtime error
Runtime error
| title: Waterleaf | |
| emoji: 🌿 | |
| colorFrom: green | |
| colorTo: red | |
| sdk: docker | |
| app_port: 7860 | |
| hf_oauth: true | |
| hf_oauth_expiration_minutes: 43200 | |
| license: mit | |
| short_description: Plant ID and weather-aware watering calendars | |
| tags: | |
| - track:backyard | |
| - sponsor:modal | |
| - sponsor:openai | |
| - achievement:llama | |
| - achievement:fieldnotes | |
| - achievement:bestdemo | |
| # Waterleaf | |
| Waterleaf identifies an outdoor garden plant from one to three photographs, | |
| grounds the result in a plant taxonomy database, builds an editable | |
| weather-aware watering plan, and exports one 30-day calendar for the garden. | |
| Built by Hakan Karaoguz (`hkaraoguz`) for the 2026 Hugging Face Build Small | |
| Hackathon. | |
| ## Workflow | |
| 1. Upload or capture one to three photographs of one plant. | |
| 2. Gemma 4 extracts visible traits and proposes likely names. | |
| 3. GBIF resolves those names to valid plant species. | |
| 4. Gemma reranks only the valid records. | |
| 5. Confirm or replace the species through autocomplete. | |
| 6. Preview weather-adjusted dates and edit them. | |
| 7. Save plants and export one whole-garden ICS file. | |
| ## Architecture | |
| - **UI and web:** Gradio Blocks mounted in FastAPI | |
| - **Authentication:** Hugging Face OAuth | |
| - **Persistence:** SQLite and normalized JPEGs on an attached HF Storage Bucket | |
| - **Vision model:** `ggml-org/gemma-4-26B-A4B-it-GGUF` | |
| - **Runtime:** llama.cpp `server-cuda13-b9445` on a Modal L4 | |
| - **Taxonomy:** GBIF Species API | |
| - **Care data:** Small local baseline catalog with manual interval fallback | |
| - **Weather:** Open-Meteo geocoding and 16-day forecast | |
| - **Calendar:** RFC 5545-compatible ICS with stable UIDs, alarms, profile URLs, | |
| and image attachments | |
| See [docs/architecture.md](docs/architecture.md) for the data flow and privacy | |
| boundaries. | |
| ## Local Development | |
| Python 3.11-3.13 and `uv` are supported. | |
| ```bash | |
| uv sync | |
| uv run uvicorn app:app --host 0.0.0.0 --port 7860 | |
| ``` | |
| Open `http://localhost:7860`. Without `MODAL_ENDPOINT`, Waterleaf uses a | |
| deterministic lavender demo identifier. Local persistence uses the | |
| `local-gardener` identity and `data/` directory. | |
| Run checks: | |
| ```bash | |
| uv sync | |
| uv run pytest | |
| uv run ruff check . | |
| ``` | |
| ## Modal Deployment | |
| ### 1. Authenticate the Modal CLI | |
| Install the deploy dependency and connect the local CLI to the Modal workspace: | |
| ```bash | |
| uv sync --group deploy | |
| uv run --group deploy modal setup | |
| ``` | |
| ### 2. Deploy llama.cpp | |
| Deploy the protected GPU service: | |
| ```bash | |
| uv run --group deploy modal deploy modal_app.py | |
| ``` | |
| The Modal service: | |
| - uses the pinned `ghcr.io/ggml-org/llama.cpp:server-cuda13-b9445` image; | |
| - starts `Gemma 4 26B-A4B Q4_K_M` with automatic multimodal projector download; | |
| - uses an 8K context, full GPU offload, Flash Attention, Q8 KV cache, and one | |
| parallel slot; | |
| - uses a bounded 256-token thinking pass for database-candidate reranking while | |
| keeping initial visual extraction non-thinking and schema-constrained; | |
| - caches Hugging Face artifacts in a Modal Volume; | |
| - requires Modal proxy-auth headers. | |
| The command prints the `modal.run` URL. Save it as `MODAL_ENDPOINT`. | |
| For the live demo and judging window, keep one container warm: | |
| ```bash | |
| MODAL_MIN_CONTAINERS=1 uv run --group deploy modal deploy modal_app.py | |
| ``` | |
| Return to zero warm containers after judging to stop idle GPU spend: | |
| ```bash | |
| MODAL_MIN_CONTAINERS=0 uv run --group deploy modal deploy modal_app.py | |
| ``` | |
| ### 3. Create proxy credentials | |
| In Modal Workspace Settings, create a **Web endpoint proxy auth token**. Save | |
| the token ID as `MODAL_KEY` and token secret as `MODAL_SECRET`. These are not | |
| the same credentials used by `modal setup`. | |
| Test the endpoint before configuring the Space: | |
| ```bash | |
| MODAL_ENDPOINT=https://...modal.run \ | |
| MODAL_KEY=wk-... \ | |
| MODAL_SECRET=ws-... \ | |
| uv run python scripts/smoke_modal.py assets/sample-lavender.png | |
| ``` | |
| If a proxy token has not been created yet, a controlled one-time smoke test can | |
| temporarily publish the endpoint: | |
| ```bash | |
| MODAL_PROXY_AUTH=0 MODAL_MIN_CONTAINERS=1 \ | |
| uv run --group deploy modal deploy modal_app.py | |
| MODAL_ENDPOINT=https://...modal.run \ | |
| uv run python scripts/smoke_modal.py assets/sample-lavender.png | |
| MODAL_PROXY_AUTH=1 MODAL_MIN_CONTAINERS=0 \ | |
| uv run --group deploy modal deploy modal_app.py | |
| ``` | |
| The middle deployment is unauthenticated and should exist only for the smoke | |
| test. Always run the final restore command immediately afterward. | |
| ## Space Configuration | |
| ### 1. Create the Space | |
| Create `build-small-hackathon/waterleaf` in the Hugging Face UI with: | |
| - **SDK:** Docker | |
| - **Visibility:** Public | |
| - **License:** MIT | |
| If the hackathon organization does not allow direct creation, create | |
| `hkaraoguz/waterleaf` first and transfer or duplicate it into the requested | |
| hackathon namespace. | |
| The root README metadata already enables Docker on port `7860` and HF OAuth. | |
| ### 2. Upload this repository | |
| Authenticate the Hugging Face CLI and upload the working tree. The CLI sends | |
| binary assets through Xet storage, which a plain Git push does not: | |
| ```bash | |
| hf auth login | |
| hf upload build-small-hackathon/waterleaf . . \ | |
| --repo-type space \ | |
| --exclude '.git/**' \ | |
| --exclude '.venv/**' \ | |
| --exclude '.pytest_cache/**' \ | |
| --exclude '.ruff_cache/**' \ | |
| --exclude '**/__pycache__/**' \ | |
| --exclude '*.pyc' \ | |
| --exclude 'data/**' \ | |
| --exclude '.env' | |
| ``` | |
| Do not commit or upload local environment files or deployment secrets. | |
| ### 3. Attach persistent storage | |
| In **Space Settings → Storage Buckets**: | |
| 1. Create or select a bucket for Waterleaf. | |
| 2. Attach it read-write. | |
| 3. Set the mount path to `/data`. | |
| The Docker image already sets `WATERLEAF_DATA_DIR=/data`. Without this mount, | |
| saved gardens and images disappear when the Space restarts. | |
| ### 4. Configure secrets and variables | |
| In **Space Settings → Variables and secrets**, add: | |
| | Name | Type | Required | Purpose | | |
| | --- | --- | --- | --- | | |
| | `MODAL_ENDPOINT` | Secret | Production | Protected llama.cpp base URL | | |
| | `MODAL_KEY` | Secret | Production | Modal proxy token ID | | |
| | `MODAL_SECRET` | Secret | Production | Modal proxy token secret | | |
| | `PUBLIC_BASE_URL` | Variable | Optional | Override the derived Space URL | | |
| | `WATERLEAF_DATA_DIR` | Variable | No | Defaults to `/data` in Docker | | |
| Use only the base Modal URL for `MODAL_ENDPOINT`; do not append | |
| `/v1/chat/completions`. | |
| ### 5. Rebuild and verify | |
| Trigger **Factory reboot** after attaching storage or changing secrets. Then | |
| verify: | |
| ```bash | |
| curl --fail https://build-small-hackathon-waterleaf.hf.space/health | |
| ``` | |
| Expected response: | |
| ```json | |
| {"status":"ok"} | |
| ``` | |
| Open the Space directly, not only inside the Hub iframe, and check: | |
| 1. **Sign in with Hugging Face** completes successfully. | |
| 2. A guest can preview one identification. | |
| 3. A signed-in user can save a plant and see it after a factory restart. | |
| 4. The generated ICS downloads and its public plant/image links open. | |
| Guests may run one temporary identification preview. Login is required to | |
| save, delete, or export plants. | |
| ## Evaluation | |
| Populate `evaluation/manifest.csv` with at least 20 consented real-garden | |
| examples and run: | |
| ```bash | |
| MODAL_ENDPOINT=... MODAL_KEY=... MODAL_SECRET=... \ | |
| uv run python scripts/evaluate.py evaluation/manifest.csv | |
| ``` | |
| The report includes species top-1/top-3 accuracy, genus top-1 accuracy, | |
| per-case predictions, and latency. A live one-to-three-photo smoke test is | |
| available at `scripts/smoke_modal.py`. | |
| ## Privacy and Limitations | |
| - Images are resized, converted to JPEG, and stripped of EXIF. | |
| - Stored coordinates are rounded and never exposed on public plant pages. | |
| - Public pages use opaque slugs but are intentionally public for calendar use. | |
| - Plant identification and watering dates are suggestions, not horticultural | |
| guarantees. | |
| - If forecast retrieval fails after location resolution, Waterleaf still | |
| generates dates from the care baseline and labels them as seasonal estimates. | |
| - Geocoding failure stops schedule preview until the location is corrected. | |
| - Dates after the 16-day forecast are labeled seasonal estimates. | |
| - ICS `ATTACH` support varies by calendar client; every event also includes a | |
| portable public profile link. | |
| - Users must provide a manual interval when the local catalog has no care | |
| baseline for the selected species. | |
| ## Submission Materials | |
| - [30-second demo video](https://www.youtube.com/watch?v=4H5vGVFcaO4) | |
| - [X post](https://x.com/hknkrgz/status/2066605985741807972) | |
| - [Field Notes](docs/field-notes.md) | |
| - [Architecture](docs/architecture.md) | |
| Target categories: Backyard AI, Best Demo, Llama Champion, Modal-powered, and | |
| Field Notes. Waterleaf does not claim Off the Grid because inference, taxonomy, | |
| and weather are cloud-hosted. | |
| ## Team | |
| - [Hakan Karaoguz (`hkaraoguz`)](https://huggingface.co/hkaraoguz) | |
| ## Credits | |
| - [Gemma 4](https://huggingface.co/google/gemma-4-26B-A4B-it) | |
| - [llama.cpp](https://github.com/ggml-org/llama.cpp) | |
| - [GBIF](https://www.gbif.org/developer/species) | |
| - [Open-Meteo](https://open-meteo.com/) | |