bloombase/docs/superpowers/specs/2026-05-30-plantnet-design.md
Stephan Kerkman ed21c41b53 docs: add Pl@ntNet photo ID design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:58:40 +02:00

226 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Pl@ntNet Photo Identification — Design Spec
**Date:** 2026-05-30
**Status:** Approved
## Overview
Replace the Perenual + VPC text-search entry point with a Pl@ntNet photo-identification flow. User photographs an unknown plant, Pl@ntNet returns ranked species matches, and the app enriches the result with VPC (Dutch nursery) data via a per-field picker. Card scan (OCR) flow is unchanged.
Also in scope: fix the misaligned delete button on the plant detail page.
---
## Scope
**In scope:**
- New 3-step photo ID flow (`/identify/`)
- Pl@ntNet API service wrapper
- VPC enrichment after species confirm (auto-search by scientific name)
- Field picker UI (source selection per field)
- Nav update: "+" → `/identify/`
- Remove Perenual from species_search UI (service file kept, unused)
- Delete button alignment fix on `plant_detail.html`
- `.superpowers/` added to `.gitignore`
**Out of scope:**
- Removing `perenual.py` service file
- Changes to card scan / OCR flow
- New Species model fields (no `plantnet_id` in this iteration)
---
## Architecture
### New service: `plants/services/plantnet.py`
```python
identify(images: list[tuple[bytes, str]]) -> list[dict]
```
- Each tuple: `(image_bytes, organ)` where organ is one of `flower | leaf | fruit | bark | habit | auto`
- Calls `POST https://my-api.plantnet.org/v2/identify/all?api-key=<key>&lang=en`
- Returns up to 5 ranked matches, each:
```python
{
"scientific_name": str,
"common_names": list[str], # English + NL if available
"score": float, # 01 confidence
"gbif_id": str | None,
"family": str,
"genus": str,
}
```
- Raises `PlantNetError` on API failure, quota exceeded, or 0 results
- `PLANTNET_API_KEY` read from Django settings (env var)
### New view module: `plants/views/identify.py`
Three views, each handling one step of the flow.
### URL structure
| URL | View | Method | Purpose |
|---|---|---|---|
| `/identify/` | `identify_upload` | GET | Render upload form |
| `/identify/` | `identify_upload` | POST | Save images, call Pl@ntNet, redirect to step 2 |
| `/identify/confirm/` | `identify_confirm` | GET | Render species picker |
| `/identify/confirm/` | `identify_confirm` | POST | Store selection, run VPC search, redirect to step 3 |
| `/identify/fields/` | `identify_fields` | GET | Render field picker |
| `/identify/fields/` | `identify_fields` | POST | Create Species + Plant, cleanup, redirect to plant detail |
### Session keys
| Key | Content | Set at | Cleared at |
|---|---|---|---|
| `identify_matches` | list of Pl@ntNet match dicts | step 1 POST | step 3 POST |
| `identify_image_paths` | list of `{"path": str, "organ": str}` | step 1 POST | step 3 POST |
| `identify_selected` | single match dict | step 2 POST | step 3 POST |
| `identify_vpc_results` | list of VPC result dicts (top 3) | step 2 POST | step 3 POST |
| `identify_vpc_idx` | int index of selected VPC result (default 0) | step 3 GET (change) | step 3 POST |
### Temp file storage
Images saved to `plants/temp_identify/<uuid>_<n>.jpg` via `default_storage`. Deleted unconditionally in step 3 POST regardless of outcome (success or exception).
---
## Step 1 — Upload
**Template:** `plants/templates/plants/identify_upload.html`
- Multi-photo picker (up to 5). Each photo shows a thumbnail row with:
- 72×72 px thumbnail
- Organ chips inline: 🌸 flower · 🍃 leaf · 🌿 habit · 🍎 fruit · 🪵 bark · ✨ auto
- Active chip highlighted green, tap to change
- ✕ to remove photo
- Default organ: `auto`
- "Add another photo" dashed button (hidden when 5 photos selected)
- "Identify →" submit button (disabled until ≥1 photo)
- "Or enter name manually" text link → `/plant/add/` (existing VPC text search, secondary path)
- Full-screen spinner overlay during submit (same pattern as card scan)
**POST handling:**
- Validate: at least 1 image, JPEG or PNG only, ≤50 MB total
- Save each image to `plants/temp_identify/<uuid>_<n>.jpg`
- Call `plantnet.identify(images)` with per-image organ tags
- On success: store `identify_matches` + `identify_image_paths` in session → redirect to `/identify/confirm/`
- On `PlantNetError`: delete any saved temp images, re-render step 1 with error banner ("Identification failed — try again or enter name manually"). Quota exceeded shown as "Daily limit reached (500/day)".
---
## Step 2 — Species confirm
**Template:** `plants/templates/plants/identify_confirm.html`
- Guard: if `identify_matches` not in session → redirect to `/identify/`
- Shows ranked matches (up to 5) as selectable cards:
- Scientific name (bold)
- Common name(s)
- Confidence badge (e.g. "94%") — green ≥70%, amber 3069%, grey <30%
- Species image placeholder (Pl@ntNet does not return images in identify response)
- Top match pre-selected (green border)
- "Use [species name] →" confirm button
- "← Try again" link back to step 1
**POST handling:**
- Store selected match as `identify_selected` in session
- Run VPC `search_species(scientific_name)`, take top 3 results → store as `identify_vpc_results`
- If VPC raises or returns empty: store `identify_vpc_results = []` (non-blocking)
- Redirect to `/identify/fields/`
---
## Step 3 — Field picker
**Template:** `plants/templates/plants/identify_fields.html`
- Guard: if `identify_selected` not in session → redirect to `/identify/`
### VPC banner
- If VPC results found: green banner showing top match name + "vasteplantencatalogus.nl", with "change" link. Clicking "change" posts `vpc_idx` (integer) back to `/identify/fields/` GET, storing it in the session and re-rendering with the new VPC selection. Other VPC results shown as selectable rows in a small inline list (no JS required).
- If no VPC results: amber banner "No VPC data found — using Pl@ntNet only"
Add session key `identify_vpc_idx` (int, default 0) — index into `identify_vpc_results` list for the currently selected VPC match.
### Two-source fields (common name, scientific name)
Side-by-side tiles. Active tile has green border + "✓ selected" label. Tap inactive tile to switch.
| Field | Pl@ntNet value | VPC value | Default |
|---|---|---|---|
| `common_name` | English common name (+ NL if available) | Dutch store/cultivar name | VPC |
| `scientific_name` | Authoritative species name | May include cultivar | Pl@ntNet |
### VPC-only care fields (shown as confirmation tiles, not a choice)
Displayed only when VPC match found. Read-only grid of 2 columns:
- Bloom months (dot strip, same as existing month strip)
- Height (cm)
- Sunlight
- Frost hardiness (°C)
- Planting density (/m²)
### Plant name input
Free-text, required. Pre-filled with Pl@ntNet scientific name (user edits to their preferred name e.g. "Liatris").
### Save
POST looks up existing `Species` via `Species.objects.filter(scientific_name=<exact>).first()` — reuses if found, creates otherwise. (`scientific_name` has no unique constraint in the model.) Creates `Plant` linked to species. Deletes all temp images (try/finally) and clears all `identify_*` session keys. Redirects to `plant_detail`.
---
## Nav changes
`base.html` — "+" button `href` changed from `{% url 'plant_add' %}` to `{% url 'identify_upload' %}`.
`species_search.html` — remove the `include_perenual` checkbox and Perenual toggle. VPC search remains. Page still accessible via "enter name manually" fallback.
---
## Delete button fix
`plant_detail.html` — current layout has action buttons (Plant card, Crop thumbnail, Delete plant) as separate inline elements without a flex container. Fix: wrap all three in `<div class="d-flex flex-wrap gap-2 mt-2">`. Delete button keeps `btn-outline-danger`.
---
## Error handling
| Scenario | Behaviour |
|---|---|
| Pl@ntNet API error / timeout | Re-render step 1 with error banner |
| Pl@ntNet quota exceeded | Re-render step 1: "Daily limit reached (500/day)" |
| 0 results from Pl@ntNet | Re-render step 1: "No match found — try a different photo" |
| Invalid image format/size | Client `accept="image/jpeg,image/png"` + server re-renders step 1 with validation error |
| VPC search fails | Non-blocking — step 3 renders in no-VPC-match state |
| Direct navigation to step 2/3 without session | Redirect to step 1 |
| Step 3 POST exception (Species/Plant save fails) | Temp files still cleaned up (try/finally) |
---
## Settings
Add to `settings.py` (read from env):
```python
PLANTNET_API_KEY = env("PLANTNET_API_KEY", default="")
```
Add `PLANTNET_API_KEY` to `.env` on dockerhost.
---
## `.gitignore`
Add `.superpowers/` to project `.gitignore`.
---
## Out of scope / future
- Perenual removal (service file unused but harmless)
- `Species.plantnet_id` field (can add in a later migration once data quality is validated)
- Caching Pl@ntNet results (500/day is sufficient for personal use)
- Multilingual common names stored per-language on Species model