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

8.7 KiB
Raw Permalink Blame History

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

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:
    {
        "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):

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