bloombase/plants/views/identify.py

220 lines
7.7 KiB
Python

import logging
import os
import uuid
import requests
from django.shortcuts import render, redirect
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from plants.services import plantnet
from plants.services.plantnet import PlantNetError
from plants.services.vpc import search_species as vpc_search
from plants.models import Plant, PlantPhoto, Species, Location
logger = logging.getLogger(__name__)
_SESSION_KEYS = [
'identify_matches', 'identify_image_paths', 'identify_selected',
'identify_vpc_results', 'identify_vpc_idx',
]
_VALID_ORGANS = {'flower', 'leaf', 'fruit', 'bark', 'habit', 'auto'}
def _clear_session(request):
for key in _SESSION_KEYS:
request.session.pop(key, None)
def identify_upload(request):
if request.method != 'POST':
return render(request, 'plants/identify_upload.html', {'active_tab': 'home'})
files = request.FILES.getlist('images')
organs = request.POST.getlist('organs')
if not files:
return render(request, 'plants/identify_upload.html',
{'error': 'Please select at least one photo.', 'active_tab': 'home'})
scan_id = uuid.uuid4().hex
saved_paths = []
images = []
for i, f in enumerate(files[:5]):
organ = organs[i] if i < len(organs) else 'auto'
if organ not in _VALID_ORGANS:
organ = 'auto'
file_bytes = f.read()
name = f'plants/temp_identify/{scan_id}_{i}.jpg'
default_storage.save(name, ContentFile(file_bytes))
saved_paths.append({'path': name, 'organ': organ})
images.append((file_bytes, organ))
error_msg = None
try:
matches = plantnet.identify(images)
except PlantNetError as e:
error_msg = str(e)
except Exception as e:
logger.exception('identify_upload unexpected error: %s', e)
error_msg = 'Identification failed — please try again.'
if error_msg:
for p in saved_paths:
try:
if default_storage.exists(p['path']):
default_storage.delete(p['path'])
except Exception:
pass
return render(request, 'plants/identify_upload.html', {'error': error_msg, 'active_tab': 'home'})
request.session['identify_matches'] = matches
request.session['identify_image_paths'] = saved_paths
return redirect('identify_confirm')
def identify_confirm(request):
if 'identify_matches' not in request.session:
return redirect('identify_upload')
matches = request.session['identify_matches']
if not matches:
return redirect('identify_upload')
if request.method == 'POST':
try:
idx = int(request.POST.get('match_idx', 0))
idx = max(0, min(idx, len(matches) - 1))
except (ValueError, TypeError):
idx = 0
selected = matches[idx]
request.session['identify_selected'] = selected
try:
vpc_results = vpc_search(selected['scientific_name'])[:3]
except Exception:
vpc_results = []
request.session['identify_vpc_results'] = vpc_results
request.session['identify_vpc_idx'] = 0
return redirect('identify_fields')
locations = list(Location.objects.all())
return render(request, 'plants/identify_confirm.html', {
'matches': matches,
'locations': locations,
})
def _apply_vpc_care_fields(species, vpc_result):
from plants.services.vpc import scrape_plant, VPCError
slug = vpc_result.get('slug', '')
if not slug:
return
try:
data = scrape_plant(slug)
except VPCError:
return
for field in ('bloom_months', 'max_height_cm', 'sunlight', 'growth_rate',
'description', 'api_image_url', 'frost_hardiness_c',
'planting_density_m2'):
val = data.get(field)
if val not in (None, '', []):
setattr(species, field, val)
species.vpc_slug = slug
species.vpc_raw = data
species.save()
def identify_fields(request):
if 'identify_selected' not in request.session:
return redirect('identify_upload')
selected = request.session['identify_selected']
vpc_results = request.session.get('identify_vpc_results', [])
vpc_idx = request.session.get('identify_vpc_idx', 0)
if request.method == 'GET' and 'vpc_idx' in request.GET:
try:
new_idx = int(request.GET['vpc_idx'])
vpc_idx = max(0, min(new_idx, len(vpc_results) - 1))
request.session['identify_vpc_idx'] = vpc_idx
except (ValueError, TypeError):
pass
return redirect('identify_fields')
vpc_match = vpc_results[vpc_idx] if vpc_results else None
if request.method == 'POST':
common_name_source = request.POST.get('common_name_source', 'plantnet')
scientific_name_source = request.POST.get('scientific_name_source', 'plantnet')
name = request.POST.get('name', '').strip() or selected['scientific_name']
if common_name_source == 'vpc' and vpc_match:
common_name = vpc_match['common_name']
else:
common_name = (selected['common_names'] or [selected['scientific_name']])[0]
if scientific_name_source == 'vpc' and vpc_match:
scientific_name = vpc_match['scientific_name']
else:
scientific_name = selected['scientific_name']
species = Species.objects.filter(scientific_name=scientific_name).first()
if not species:
species = Species.objects.create(
scientific_name=scientific_name,
common_name=common_name,
)
else:
species.common_name = common_name
species.save(update_fields=['common_name'])
if vpc_match:
_apply_vpc_care_fields(species, vpc_match)
plant = Plant.objects.create(name=name, species=species)
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', [])
for i, p in enumerate(image_paths):
if not default_storage.exists(p['path']):
continue
try:
with default_storage.open(p['path'], 'rb') as fh:
data = fh.read()
filename = os.path.basename(p['path'])
photo = PlantPhoto(plant=plant, is_thumbnail=(i == 0 and not plantnet_img_url))
photo.image.save(filename, ContentFile(data), save=True)
except Exception:
logger.exception('identify_fields: failed to save photo %s', p['path'])
finally:
try:
default_storage.delete(p['path'])
except Exception:
pass
finally:
_clear_session(request)
return redirect('plant_detail', pk=plant.pk)
return render(request, 'plants/identify_fields.html', {
'selected': selected,
'vpc_results': vpc_results,
'vpc_idx': vpc_idx,
'vpc_match': vpc_match,
})