Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 8 additions & 54 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
# Inference/fusion require PyTorch — import lazily so server starts without it
try:
from inference import load_models, predict_stream_a, predict_stream_b
from fusion import process_and_fuse
from backend.fusion import process_and_fuse

_torch_available = True
except ModuleNotFoundError:
_torch_available = False
print("WARNING: PyTorch not installed. Scan endpoints will return 503.")

from auth import get_current_user, get_google_oauth_url, exchange_code_for_session
from backend.auth import get_current_user, get_google_oauth_url, exchange_code_for_session

# ── Configuration ─────────────────────────────────────────────────────────────
# All secrets MUST come from environment variables — no hardcoded fallbacks.
Expand Down Expand Up @@ -100,16 +100,8 @@ async def lifespan(app: FastAPI):
if CORS_ALLOW_ALL
else [
FRONTEND_URL,
# Current production frontend — always allow so a stale FRONTEND_URL
# env var doesn't lock out users.
"https://fresh-scanai.vercel.app",
# Extra comma-separated origins from env (e.g. preview deployments).
# ADDITIONAL_CORS_ORIGINS=https://preview.vercel.app,https://staging.example.com
*[
o.strip()
for o in os.environ.get("ADDITIONAL_CORS_ORIGINS", "").split(",")
if o.strip()
],
# Always allow Vercel preview deployments
"https://fresh-scan-ai-sage.vercel.app",
]
)

Expand Down Expand Up @@ -514,7 +506,7 @@ async def scan_auto(
return {"success": True, "scan": payload}

# ── Real inference path ───────────────────────────────────────────────────
from router import classify_image_type, ImageType
from backend.router import classify_image_type, ImageType

img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
image_type = classify_image_type(img)
Expand Down Expand Up @@ -671,46 +663,6 @@ async def get_vendors():
raise HTTPException(status_code=500, detail=str(exc))


@app.get("/api/v1/vendors/leaderboard")
async def get_leaderboard():
try:
fields = "id, name, address, avg_freshness_score, total_scans"
resp = (
_db()
.table("vendors")
.select(fields)
.order("avg_freshness_score", desc=True)
.limit(10)
.execute()
)

leaderboard = []
for v in (resp.data or []):
score = v.get("avg_freshness_score") or 0
if score >= 85:
badge = "gold"
elif score >= 70:
badge = "silver"
elif score >= 50:
badge = "bronze"
else:
badge = "unranked"

leaderboard.append({
"id": v["id"],
"name": v["name"],
"address": v["address"] or "Unknown Location",
"avg_freshness_score": score,
"total_scans": v.get("total_scans") or 0,
"trust_badge": badge,
"trend": "stable",
})

return {"success": True, "leaderboard": leaderboard}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))


# ── MAP ───────────────────────────────────────────────────────────────────────


Expand Down Expand Up @@ -843,7 +795,7 @@ async def generate_gradcam(
# ── Real Grad-CAM path ──────────────────────────────────────────────────────
import torch # noqa: F401
import numpy as np
from inference import stream_a_model, stream_a_transforms, device
from backend.inference import stream_a_model, stream_a_transforms, device
from router import is_valid_fish_image

# Fish validity gate — same gate used by /api/v1/scan-auto
Expand Down Expand Up @@ -933,4 +885,6 @@ def _bwd_hook(_module, _grad_in, grad_out):
from vendors import router as vendors_router, register_routes

register_routes(vendors_router, _db)
from markets import router as markets_router
app.include_router(markets_router)
app.include_router(vendors_router)
119 changes: 119 additions & 0 deletions backend/markets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Real-world fish market locations via OpenStreetMap Overpass API.
Endpoint: GET /api/v1/maps/markets/live?lat=...&lng=...&radius=5000
"""

import httpx
import math
from fastapi import APIRouter, Query

router = APIRouter(prefix="/api/v1/maps/markets", tags=["markets"])

OVERPASS_URL = "https://overpass-api.de/api/interpreter"

OVERPASS_QUERY_TEMPLATE = """
[out:json][timeout:25];
(
node["shop"="seafood"]({south},{west},{north},{east});
node["shop"="fish"]({south},{west},{north},{east});
node["amenity"="marketplace"]["name"~"fish|seafood|market",i]({south},{west},{north},{east});
node["landuse"="retail"]["name"~"fish|seafood",i]({south},{west},{north},{east});
);
out body;
"""


def _lat_lng_to_bbox(lat: float, lng: float, radius_m: float):
"""Convert center + radius to bounding box (south, west, north, east)."""
delta_lat = radius_m / 111320
import math
delta_lng = radius_m / (111320 * abs(math.cos(math.radians(lat))) + 1e-9)
return {
"south": lat - delta_lat,
"west": lng - delta_lng,
"north": lat + delta_lat,
"east": lng + delta_lng,
}


def _parse_overpass(elements: list, fallback_score: int = 70) -> list:
"""Normalize Overpass API elements into the MarketMapPage format."""
markets = []
for i, el in enumerate(elements):
tags = el.get("tags", {})
name = (
tags.get("name")
or tags.get("name:en")
or tags.get("shop")
or "Fish Market"
)
lat = el.get("lat")
lon = el.get("lon")
if lat is None or lon is None:
continue
markets.append({
"id": el.get("id", i + 1),
"name": name,
"score": fallback_score,
"lat": float(lat),
"lng": float(lon),
"vendors": 1,
"source": "openstreetmap",
"address": tags.get("addr:full") or tags.get("addr:street") or "",
})
return markets


@router.get("/live")
async def get_live_markets(
lat: float = Query(..., description="Latitude of user location"),
lng: float = Query(..., description="Longitude of user location"),
radius: int = Query(default=5000, ge=500, le=50000, description="Search radius in meters"),
):
"""
Fetch real-world fish markets near a location using OpenStreetMap Overpass API.
Falls back to empty list if Overpass is unavailable.
"""
bbox = _lat_lng_to_bbox(lat, lng, radius)
query = OVERPASS_QUERY_TEMPLATE.format(**bbox)

try:
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(
OVERPASS_URL,
data={"data": query},
headers={
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "FreshScanAI/1.0 (https://github.com/jpdevhub/FreshScanAi)",
},
)
response.raise_for_status()
data = response.json()

elements = data.get("elements", [])
markets = _parse_overpass(elements)

return {
"success": True,
"source": "openstreetmap",
"count": len(markets),
"lat": lat,
"lng": lng,
"radius_m": radius,
"markets": markets,
}

except httpx.TimeoutException:
return {
"success": False,
"source": "openstreetmap",
"error": "Overpass API timed out. Try again or reduce radius.",
"markets": [],
}
except Exception as exc:
return {
"success": False,
"source": "openstreetmap",
"error": str(exc),
"markets": [],
}
Loading
Loading