docs: add Pl@ntNet photo ID design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephan Kerkman 2026-05-30 16:58:40 +02:00
parent ba0ca6b169
commit ed21c41b53

View file

@ -0,0 +1,226 @@
# 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