bloombase/plants/views/identify.py

174 lines
5.8 KiB
Python

import uuid
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, Species
_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')
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.'})
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))
try:
matches = plantnet.identify(images)
except PlantNetError as e:
for p in saved_paths:
if default_storage.exists(p['path']):
default_storage.delete(p['path'])
return render(request, 'plants/identify_upload.html', {'error': str(e)})
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')
return render(request, 'plants/identify_confirm.html', {'matches': matches})
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:
for p in request.session.get('identify_image_paths', []):
if default_storage.exists(p['path']):
default_storage.delete(p['path'])
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,
})