From ed21c41b53a90d88d540d743bdd5d1b5772eceaa Mon Sep 17 00:00:00 2001 From: Stephan Kerkman Date: Sat, 30 May 2026 16:58:40 +0200 Subject: [PATCH] docs: add Pl@ntNet photo ID design spec Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-05-30-plantnet-design.md | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-30-plantnet-design.md diff --git a/docs/superpowers/specs/2026-05-30-plantnet-design.md b/docs/superpowers/specs/2026-05-30-plantnet-design.md new file mode 100644 index 0000000..ad294fb --- /dev/null +++ b/docs/superpowers/specs/2026-05-30-plantnet-design.md @@ -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=&lang=en` +- Returns up to 5 ranked matches, each: + ```python + { + "scientific_name": str, + "common_names": list[str], # English + NL if available + "score": float, # 0–1 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/_.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/_.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 30–69%, 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=).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 `
`. 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