""" Location Intelligence service — estimates where an image was captured. Combines evidence from: - GPS (from EXIF metadata, via cores.metadata) - OCR text (language + keyword detection) - License plate format - Scene labels - Detected objects (vehicle presence = street/road context) Returns a LocationEstimate with candidate countries, cities, and conflicting evidence. No heavy geolocation models — pure heuristics. """ from __future__ import annotations import time from typing import Optional from cores.location import LocationEvidenceCollector, LocationEstimator from cores.metadata import extract_all from models.jobs import JobRequest from models.reports import LocationEstimate from pipeline import InputValidator, ImagePreprocessor, ImageHasher from utils.logging import execution_context, new_execution_id class LocationIntelligenceService: """Estimates image capture location from multiple evidence sources.""" def __init__( self, validator: InputValidator, preprocessor: ImagePreprocessor, hasher: ImageHasher, ) -> None: self._validator = validator self._preprocessor = preprocessor self._hasher = hasher async def analyze(self, request: JobRequest) -> dict: """Estimate location from an image.""" eid = new_execution_id() with execution_context(execution_id=eid, provider_id="location_intelligence_service"): t0 = time.perf_counter() vr = self._validator.validate( image_url=request.image_url, image_base64=request.image_base64, ) if not vr.valid: return {"success": False, "error": vr.error, "error_type": "ValidationError"} if vr.source == "url": pre = self._preprocessor.from_url(request.image_url) else: pre = self._preprocessor.from_bytes(vr.image_bytes, vr.source) original_bytes = pre.original_bytes or vr.image_bytes collector = LocationEvidenceCollector() gps: Optional[dict] = None # 1. GPS from EXIF if original_bytes: meta = extract_all(original_bytes) if meta.get("gps_coords"): gps = meta["gps_coords"] collector.add_gps(gps["lat"], gps["lon"]) # 2. OCR text — if the caller passed it in options # (We don't run OCR here; the caller can pass OCR results via options) ocr_text = request.options.get("ocr_text", "") if ocr_text: collector.add_ocr_text(ocr_text) # 3. License plate — if passed in options plate_text = request.options.get("plate_text", "") if plate_text: collector.add_plate(plate_text) # 4. Scene label — if passed in options scene_label = request.options.get("scene_label", "") if scene_label: collector.add_scene(scene_label) # 5. Detected objects — if passed in options objects = request.options.get("objects", []) if objects: collector.add_detected_objects(objects) # Estimate estimator = LocationEstimator() estimate = estimator.estimate(collector.evidence, gps=gps) elapsed = (time.perf_counter() - t0) * 1000.0 estimate.elapsed_ms = round(elapsed, 3) return { "success": True, "location_estimate": estimate.model_dump(), "elapsed_ms": round(elapsed, 3), }