# app.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Dict, Any import joblib, os, time, math import numpy as np APP_PORT = int(os.environ.get("PORT", 7860)) # HF maps to 7860 app = FastAPI(title="Transit Tracker API") # In-memory store for vehicles VEHICLES: Dict[str, Dict[str, Any]] = {} MODEL = None MODEL_PATH = "models/model.joblib" # -- Utilities def haversine(lat1, lon1, lat2, lon2): # returns meters R = 6371000 phi1, phi2 = math.radians(lat1), math.radians(lat2) dphi = math.radians(lat2-lat1) dlambda = math.radians(lon2-lon1) a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2 return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) def load_model(): global MODEL if MODEL is None: if not os.path.exists(MODEL_PATH): raise RuntimeError("Model file not found: " + MODEL_PATH) MODEL = joblib.load(MODEL_PATH) return MODEL # -- Request models class VehicleUpdate(BaseModel): vehicle_id: str lat: float lon: float speed: float = 0.0 # meters/sec bearing: float = None timestamp: str = None # ISO class PredictRequest(BaseModel): distance_to_stop_m: float speed_mps: float = 3.0 timestamp_iso: str = None # optional # -- Endpoints @app.get("/health") def health(): ok = os.path.exists(MODEL_PATH) return {"status": "ok" if ok else "model-missing", "model_path": MODEL_PATH, "time": time.time()} @app.post("/vehicle/update") def vehicle_update(v: VehicleUpdate): VEHICLES[v.vehicle_id] = { "lat": v.lat, "lon": v.lon, "speed": v.speed, "bearing": v.bearing, "timestamp": v.timestamp or time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) } return {"ok": True, "vehicle_id": v.vehicle_id} @app.get("/vehicles") def get_vehicles(): return VEHICLES @app.post("/predict") def predict(req: PredictRequest): # safety checks if req.distance_to_stop_m < 0: raise HTTPException(status_code=400, detail="distance must be >= 0") model = load_model() # compute TOD features (simple UTC seconds) if req.timestamp_iso: try: import datetime dt = datetime.datetime.fromisoformat(req.timestamp_iso.replace("Z","")) sec = dt.hour*3600 + dt.minute*60 + dt.second dow = dt.weekday() except: sec = time.gmtime().tm_hour*3600 dow = time.gmtime().tm_wday else: sec = time.gmtime().tm_hour*3600 + time.gmtime().tm_min*60 + time.gmtime().tm_sec dow = time.gmtime().tm_wday tod_sin = np.sin(2*np.pi*sec/86400) tod_cos = np.cos(2*np.pi*sec/86400) X = [[float(req.distance_to_stop_m), float(req.speed_mps), float(tod_sin), float(tod_cos), int(dow)]] eta_seconds = float(model.predict(X)[0]) return {"eta_seconds": eta_seconds, "eta_minutes": eta_seconds/60.0}