# 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