import requests from django.conf import settings _API_BASE = 'https://my-api.plantnet.org/v2/identify/all' _TIMEOUT = 15 class PlantNetError(Exception): pass def identify(images): """ Call Pl@ntNet identify API. images: list of (image_bytes, organ) tuples organ: 'flower' | 'leaf' | 'fruit' | 'bark' | 'habit' | 'auto' Returns list of match dicts (up to 5), each: scientific_name, common_names, score, gbif_id, family, genus Raises PlantNetError on any failure. """ if not settings.PLANTNET_API_KEY: raise PlantNetError('API key not configured.') files = [ ('images', (f'image{i}.jpg', img_bytes, 'image/jpeg')) for i, (img_bytes, _) in enumerate(images) ] data = [('organs', organ) for _, organ in images] try: resp = requests.post( _API_BASE, params={'api-key': settings.PLANTNET_API_KEY, 'lang': 'en'}, files=files, data=data, timeout=_TIMEOUT, ) except requests.Timeout: raise PlantNetError('Pl@ntNet timed out — try again.') if resp.status_code == 429: raise PlantNetError('Daily limit reached (500/day).') try: resp.raise_for_status() except requests.HTTPError: raise PlantNetError(f'Pl@ntNet API error ({resp.status_code}).') results = resp.json().get('results', []) if not results: raise PlantNetError('No match found — try a different photo or organ.') return [ { 'scientific_name': r['species']['scientificNameWithoutAuthor'], 'common_names': r['species'].get('commonNames', []), 'score': r['score'], 'gbif_id': (r.get('gbif') or {}).get('id'), 'family': r['species'].get('family', {}).get('scientificNameWithoutAuthor', ''), 'genus': r['species'].get('genus', {}).get('scientificNameWithoutAuthor', ''), } for r in results[:5] ]