Download api/damage.py from Unexpo/mapApp: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/Unexpo/mapApp/resolve/main/api/damage.py
- Command line
-
hf download hf://spaces/Unexpo/mapApp/api/damage.py
-
curl -L -o damage.py https://huggingface.co/spaces/Unexpo/mapApp/resolve/main/api/damage.py
5.55 kB
| import logging | |
| from typing import Optional | |
| from fastapi import APIRouter, File, UploadFile, Form, Query, HTTPException | |
| from pydantic import BaseModel | |
| from exif import Image as ExifImage | |
| from services.gemini_service import estimate_location_from_photo | |
| from services.supabase_service import save_damage_report, get_damage_reports_in_bbox | |
| logger = logging.getLogger("api_damage") | |
| router = APIRouter() | |
| # Helper to convert EXIF coordinate format to decimal degrees | |
| def convert_to_decimal(coords, ref) -> float: | |
| try: | |
| degrees = float(coords[0]) | |
| minutes = float(coords[1]) | |
| seconds = float(coords[2]) | |
| decimal = degrees + (minutes / 60.0) + (seconds / 3600.0) | |
| if ref in ['S', 'W']: | |
| decimal = -decimal | |
| return decimal | |
| except Exception as e: | |
| logger.error(f"Error converting EXIF coordinates: {e}") | |
| return 0.0 | |
| def get_exif_gps(image_bytes: bytes) -> tuple[Optional[float], Optional[float]]: | |
| """ | |
| Attempts to read GPS coordinates from image EXIF metadata. | |
| """ | |
| try: | |
| img = ExifImage(image_bytes) | |
| if not img.has_exif: | |
| return None, None | |
| lat = getattr(img, "gps_latitude", None) | |
| lat_ref = getattr(img, "gps_latitude_ref", None) | |
| lng = getattr(img, "gps_longitude", None) | |
| lng_ref = getattr(img, "gps_longitude_ref", None) | |
| if lat and lat_ref and lng and lng_ref: | |
| lat_dec = convert_to_decimal(lat, lat_ref) | |
| lng_dec = convert_to_decimal(lng, lng_ref) | |
| return lat_dec, lng_dec | |
| except Exception as e: | |
| logger.error(f"Failed to extract EXIF data: {e}") | |
| return None, None | |
| async def report_damage( | |
| file: UploadFile = File(...), | |
| severity: str = Form(...), | |
| owner_name: str = Form(...), | |
| owner_contact: str = Form(...), | |
| exact_address: str = Form(...), | |
| description: Optional[str] = Form(None), | |
| device_lat: Optional[float] = Form(None), | |
| device_lng: Optional[float] = Form(None) | |
| ): | |
| """ | |
| Submits a new damage report. Cascades coordinates lookup: | |
| 1. Direct device coordinates from PWA (HTML5 Geolocation). | |
| 2. EXIF metadata coordinates from the uploaded image. | |
| 3. Gemini Vision AI analysis of the photo context. | |
| """ | |
| try: | |
| contents = await file.read() | |
| final_lat = None | |
| final_lng = None | |
| detection_method = "unknown" | |
| ai_reasoning = None | |
| # 1. Check if device coordinates are provided by the client | |
| if device_lat is not None and device_lng is not None: | |
| final_lat = device_lat | |
| final_lng = device_lng | |
| detection_method = "device_gps" | |
| logger.info(f"Using device GPS coordinates: {final_lat}, {final_lng}") | |
| # 2. Check EXIF data if client GPS is not available | |
| if final_lat is None or final_lng is None: | |
| exif_lat, exif_lng = get_exif_gps(contents) | |
| if exif_lat is not None and exif_lng is not None: | |
| final_lat = exif_lat | |
| final_lng = exif_lng | |
| detection_method = "exif_metadata" | |
| logger.info(f"Using EXIF GPS coordinates: {final_lat}, {final_lng}") | |
| # 3. Fallback to Gemini AI Vision geolocator | |
| if final_lat is None or final_lng is None: | |
| logger.info("No GPS data found. Calling Gemini Vision AI...") | |
| mime_type = file.content_type or "image/jpeg" | |
| ai_estimate = estimate_location_from_photo(contents, mime_type) | |
| final_lat = ai_estimate.latitude | |
| final_lng = ai_estimate.longitude | |
| ai_reasoning = ai_estimate.reasoning | |
| detection_method = "gemini_vision_ai" | |
| logger.info(f"Gemini estimated coordinates: {final_lat}, {final_lng}") | |
| # In a real environment, we would upload the file to Supabase Storage and get a URL. | |
| # For this setup, we will mock the photo URL as a static link or base64 placeholder. | |
| photo_url = f"https://placeholder-url.com/uploads/{file.filename}" | |
| # Save both public damage report and private details | |
| saved_report = save_damage_report( | |
| severity=severity, | |
| latitude=final_lat, | |
| longitude=final_lng, | |
| photo_url=photo_url, | |
| status="pending", | |
| owner_name=owner_name, | |
| owner_contact=owner_contact, | |
| exact_address=exact_address, | |
| description=description | |
| ) | |
| return { | |
| "success": True, | |
| "report_id": saved_report["id"], | |
| "coordinates": { | |
| "latitude": final_lat, | |
| "longitude": final_lng | |
| }, | |
| "detection_method": detection_method, | |
| "ai_reasoning": ai_reasoning, | |
| "severity": severity, | |
| "status": "pending" | |
| } | |
| except Exception as e: | |
| logger.error(f"Error reporting damage: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def get_damage_in_view( | |
| min_lat: float = Query(...), | |
| min_lng: float = Query(...), | |
| max_lat: float = Query(...), | |
| max_lng: float = Query(...) | |
| ): | |
| """ | |
| Fetches public damage reports within the visible map bounding box. | |
| """ | |
| try: | |
| reports = get_damage_reports_in_bbox(min_lat, min_lng, max_lat, max_lng) | |
| return reports | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |