feat: show Pl@ntNet reference images on confirm screen + save as thumbnail

Extract image_url from Pl@ntNet API response (medium size). Show thumbnail
in each match card on identify_confirm. On plant creation, download the
selected match's reference image and save as PlantPhoto thumbnail. Scan
photos saved as gallery photos (non-thumbnail) with fallback to first scan
photo as thumbnail when Pl@ntNet has no image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephan Kerkman 2026-05-31 12:51:28 +02:00
parent 70c2213d98
commit 92b682a1bd
3 changed files with 22 additions and 1 deletions

View file

@ -62,6 +62,7 @@ def identify(images):
'gbif_id': (r.get('gbif') or {}).get('id'), 'gbif_id': (r.get('gbif') or {}).get('id'),
'family': r['species'].get('family', {}).get('scientificNameWithoutAuthor', ''), 'family': r['species'].get('family', {}).get('scientificNameWithoutAuthor', ''),
'genus': r['species'].get('genus', {}).get('scientificNameWithoutAuthor', ''), 'genus': r['species'].get('genus', {}).get('scientificNameWithoutAuthor', ''),
'image_url': ((r.get('images') or [{}])[0].get('url') or {}).get('m'),
} }
for r in results[:5] for r in results[:5]
] ]

View file

@ -20,6 +20,12 @@
data-idx="{{ forloop.counter0 }}" data-idx="{{ forloop.counter0 }}"
data-name="{{ m.scientific_name }}" data-name="{{ m.scientific_name }}"
onclick="selectMatch(this)"> onclick="selectMatch(this)">
{% if m.image_url %}
<img src="{{ m.image_url }}" alt="{{ m.scientific_name }}"
style="width:64px;height:64px;object-fit:cover;border-radius:8px;flex-shrink:0;">
{% else %}
<div style="width:64px;height:64px;background:#e9ecef;border-radius:8px;flex-shrink:0;"></div>
{% endif %}
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="fw-bold">{{ m.scientific_name }}</div> <div class="fw-bold">{{ m.scientific_name }}</div>
{% if m.common_names %} {% if m.common_names %}

View file

@ -1,6 +1,7 @@
import logging import logging
import os import os
import uuid import uuid
import requests
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@ -172,6 +173,19 @@ def identify_fields(request):
plant = Plant.objects.create(name=name, species=species) plant = Plant.objects.create(name=name, species=species)
try: try:
# Download Pl@ntNet reference image as thumbnail
plantnet_img_url = selected.get('image_url')
if plantnet_img_url:
try:
r = requests.get(plantnet_img_url, timeout=10)
r.raise_for_status()
thumb_filename = f'plantnet_{uuid.uuid4().hex}.jpg'
thumb = PlantPhoto(plant=plant, is_thumbnail=True)
thumb.image.save(thumb_filename, ContentFile(r.content), save=True)
except Exception:
logger.exception('identify_fields: failed to download Pl@ntNet image')
# Save uploaded scan photos; only first becomes thumbnail if no Pl@ntNet image
image_paths = request.session.get('identify_image_paths', []) image_paths = request.session.get('identify_image_paths', [])
for i, p in enumerate(image_paths): for i, p in enumerate(image_paths):
if not default_storage.exists(p['path']): if not default_storage.exists(p['path']):
@ -180,7 +194,7 @@ def identify_fields(request):
with default_storage.open(p['path'], 'rb') as fh: with default_storage.open(p['path'], 'rb') as fh:
data = fh.read() data = fh.read()
filename = os.path.basename(p['path']) filename = os.path.basename(p['path'])
photo = PlantPhoto(plant=plant, is_thumbnail=(i == 0)) photo = PlantPhoto(plant=plant, is_thumbnail=(i == 0 and not plantnet_img_url))
photo.image.save(filename, ContentFile(data), save=True) photo.image.save(filename, ContentFile(data), save=True)
except Exception: except Exception:
logger.exception('identify_fields: failed to save photo %s', p['path']) logger.exception('identify_fields: failed to save photo %s', p['path'])