Spaces:
Sleeping
Sleeping
| """ | |
| app.py — Nyvra Safety API v4.1 | |
| ================================== | |
| Dart contract (read from source): | |
| GET /predict_area?latitude=&longitude= | |
| → { safety_score:int, danger_level:str, danger_label:int, factors:[str] } | |
| GET /analyze_route?origin_lat=&origin_lng=&dest_lat=&dest_lng=&time= | |
| → { overall_score:int, | |
| routes:[ { name, duration, distance, safety_score:int, | |
| factors:[str], is_recommended:bool } ] } | |
| GET /heatmap_data?latitude=&longitude=&radius_km=&step_km= | |
| → { points:[ { lat, lng, risk:"Low"|"Medium"|"High", score:int } ], | |
| center_lat, center_lng } | |
| POST /ai { message:str } → { reply:str } | |
| GET /health | |
| Scoring v4.1: | |
| base = safe_p^0.55 * 100 * (1 - high_p^0.50) — weighted expected value | |
| thresholds: >=85 Low(GREEN) 45-84 Medium(YELLOW) <45 High(RED) | |
| time penalty (deterministic, no safe_p dependency): | |
| 8PM-4AM -30 4AM -25 5-7AM -15 6-8PM -10 else 0 | |
| Both GET and POST endpoints exist for every action endpoint | |
| because safety_api_service.dart uses POST while heatmap_service.dart uses GET. | |
| """ | |
| import datetime | |
| import logging | |
| import math | |
| import os | |
| import pickle | |
| import warnings | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from typing import List, Optional | |
| import numpy as np | |
| import pandas as pd | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from geopy.distance import geodesic | |
| from pydantic import BaseModel | |
| warnings.filterwarnings("ignore") | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") | |
| log = logging.getLogger("nyvra") | |
| # ── Optional deps ────────────────────────────────────────────────────────── | |
| _groq_key = os.environ.get("GROQ_API_KEY") | |
| try: | |
| from groq import Groq | |
| groq_client = Groq(api_key=_groq_key) | |
| log.info("Groq client ready") | |
| except Exception as _ge: | |
| groq_client = None | |
| log.warning("Groq unavailable: %s", _ge) | |
| try: | |
| import httpx as _httpx | |
| _HTTPX_OK = True | |
| except ImportError: | |
| _HTTPX_OK = False | |
| # ── App ──────────────────────────────────────────────────────────────────── | |
| app = FastAPI(title="Nyvra Safety API", version="4.1") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def root(): | |
| return { | |
| "app": "Nyvra Safety API", | |
| "version": "4.1", | |
| "status": "running", | |
| "endpoints": [ | |
| "GET /health", | |
| "GET /predict_area?latitude=&longitude=", | |
| "POST /predict_area", | |
| "GET /analyze_route?origin_lat=&origin_lng=&dest_lat=&dest_lng=", | |
| "POST /analyze_route", | |
| "GET /heatmap_data?latitude=&longitude=&radius_km=&step_km=", | |
| "POST /ai" | |
| ] | |
| } | |
| MODELS_DIR = os.path.join(os.path.dirname(__file__), "models") | |
| R_0P5 = 0.5 / 6371.0 | |
| R_1KM = 1.0 / 6371.0 | |
| R_2KM = 2.0 / 6371.0 | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # MODEL LOADING | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def _load(name: str): | |
| path = os.path.join(MODELS_DIR, f"{name}.pkl") | |
| if not os.path.exists(path): | |
| raise FileNotFoundError(f"'{path}' not found") | |
| with open(path, "rb") as f: | |
| return pickle.load(f) | |
| def _try_load(name: str): | |
| try: | |
| return _load(name), True | |
| except Exception as e: | |
| log.warning("Could not load %s: %s", name, e) | |
| return None, False | |
| print("=" * 55) | |
| print("Loading Nyvra models ...") | |
| scaler = _load("scaler") | |
| kmeans = _load("kmeans") | |
| center_lat = float(_load("center_lat")) | |
| center_lon = float(_load("center_lon")) | |
| ball_tree = _load("ball_tree") | |
| X_columns, _ok = _try_load("X_columns") | |
| if not _ok: | |
| X_columns, _ok2 = _try_load("feature_columns") | |
| if not _ok2: | |
| raise RuntimeError("Neither X_columns.pkl nor feature_columns.pkl found.") | |
| N_FEATURES = scaler.n_features_in_ | |
| log.info("Scaler expects %d features, X_columns has %d", N_FEATURES, len(X_columns)) | |
| lgb_model, lgb_ok = _try_load("lgb_model") | |
| xgb_model, xgb_ok = _try_load("xgb_model") | |
| rf_model, rf_ok = _try_load("rf_model") | |
| et_model, et_ok = _try_load("et_model") | |
| if not any([lgb_ok, xgb_ok, rf_ok]): | |
| raise RuntimeError("No usable model found (lgb/xgb/rf all missing).") | |
| _model_pool: List[tuple] = [] | |
| if lgb_ok: _model_pool.append((lgb_model, True, "LightGBM")) | |
| if xgb_ok: _model_pool.append((xgb_model, False, "XGBoost")) | |
| if rf_ok: _model_pool.append((rf_model, False, "RandomForest")) | |
| if et_ok: _model_pool.append((et_model, False, "ExtraTrees")) | |
| ENSEMBLE_OK = len(_model_pool) >= 2 | |
| print(f"Models : {[m[2] for m in _model_pool]}") | |
| print(f"Ensemble : {ENSEMBLE_OK}") | |
| print(f"Features : {N_FEATURES}") | |
| print(f"Center : ({center_lat:.4f}, {center_lon:.4f})") | |
| print("=" * 55) | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # SCHEMAS — field names must match Dart JSON keys exactly | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| class AreaRequest(BaseModel): | |
| latitude: float | |
| longitude: float | |
| hour: Optional[int] = None | |
| month: Optional[int] = None | |
| is_weekend: Optional[int] = None | |
| class RouteRequest(BaseModel): | |
| origin_lat: float | |
| origin_lng: float | |
| dest_lat: float | |
| dest_lng: float | |
| time: Optional[str] = "Now" | |
| class AreaResponse(BaseModel): | |
| safety_score: int # main.dart: _areaData['safety_score'] | |
| danger_level: str # main.dart: _areaData['danger_level'] "Low"|"Medium"|"High" | |
| danger_label: int # main.dart: _areaData['danger_label'] 0|1|2 | |
| factors: List[str] # main.dart: _areaData['factors'] | |
| class RouteResult(BaseModel): | |
| name: str # trip_planning: RouteOption.name | |
| duration: str # trip_planning: RouteOption.duration | |
| distance: str # trip_planning: RouteOption.distance | |
| safety_score: int # trip_planning: RouteOption.safetyScore | |
| factors: List[str] | |
| is_recommended: bool # trip_planning: RouteOption.isRecommended | |
| class RouteResponse(BaseModel): | |
| overall_score: int # trip_planning parses this | |
| routes: List[RouteResult] | |
| class HeatmapPoint(BaseModel): | |
| lat: float # heatmap_service: HeatmapPoint.lat | |
| lng: float # heatmap_service: HeatmapPoint.lng | |
| risk: str # heatmap_screen: _riskFill() switches on "Low"|"Medium"|"High" | |
| score: int # heatmap_screen: p.score for circle radius | |
| class HeatmapResponse(BaseModel): | |
| points: List[HeatmapPoint] | |
| center_lat: float | |
| center_lng: float | |
| class AiRequest(BaseModel): | |
| message: str # chatbot_ui: {"message": msg} | |
| class AiResponse(BaseModel): | |
| reply: str # chatbot_ui: data["reply"] | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # SCORING | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def _time_penalty(hour: int) -> int: | |
| """ | |
| Deterministic penalty by hour — no safe_p dependency. | |
| 8PM-4AM: -30 4AM: -25 5-7AM: -15 6-8PM: -10 else: 0 | |
| """ | |
| if hour >= 20 or hour < 4: | |
| return 30 | |
| if hour == 4: | |
| return 25 | |
| if 5 <= hour < 7: | |
| return 15 | |
| if 18 <= hour < 20: | |
| return 10 | |
| return 0 | |
| def _score_to_label(score: int) -> tuple: | |
| """ | |
| Consistent thresholds used across ALL endpoints. | |
| heatmap_screen.dart has its own route-segment thresholds (>=70 green, | |
| >=45 orange, <45 red) inside Flutter — those are independent of this. | |
| """ | |
| if score >= 85: | |
| return 0, "Low" | |
| elif score >= 45: | |
| return 1, "Medium" | |
| else: | |
| return 2, "High" | |
| def _score_to_factors(score: int, density: int, hour: int = 12) -> List[str]: | |
| if score >= 85: | |
| factors = ["Well-lit streets", "High foot traffic"] | |
| elif score >= 70: | |
| factors = ["Mostly safe area", "Some foot traffic"] | |
| elif score >= 55: | |
| factors = ["Moderate foot traffic", "Some isolated roads"] | |
| elif score >= 45: | |
| factors = ["Limited lighting", "Below-average safety"] | |
| else: | |
| factors = ["High crime density", "Avoid if possible"] | |
| if density > 20: | |
| factors.append(f"High incident density (~{density} nearby)") | |
| elif density > 5: | |
| factors.append(f"Moderate incidents (~{density} nearby)") | |
| else: | |
| factors.append("Low historical incidents") | |
| if hour >= 20 or hour < 4: | |
| if score < 45: | |
| factors.append("Moon High risk — avoid travel at this hour") | |
| elif score < 65: | |
| factors.append("Moon Use caution — late night travel risky") | |
| else: | |
| factors.append("Moon Relatively safer but stay alert at night") | |
| elif hour == 4: | |
| factors.append("Moon Pre-dawn hours — stay alert") | |
| elif 5 <= hour < 7: | |
| factors.append("Sun Early morning — limited activity in area") | |
| elif 18 <= hour < 20: | |
| factors.append("Evening caution advised" if score < 65 | |
| else "Moderate evening activity — stay aware") | |
| return factors | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # FEATURE ENGINEERING — matches ML_Model.ipynb Cell 5 | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def _build_feature_row( | |
| lat: float, lon: float, | |
| hour: int = 12, month: int = 6, is_weekend: int = 0, | |
| ) -> np.ndarray: | |
| dist_km = geodesic((lat, lon), (center_lat, center_lon)).km | |
| dist_sq = dist_km ** 2 | |
| lat_lon_prod = lat * lon | |
| coord_rad = np.radians([[lat, lon]]) | |
| d_0p5 = int(ball_tree.query_radius(coord_rad, r=R_0P5, count_only=True)[0]) | |
| d_1km = int(ball_tree.query_radius(coord_rad, r=R_1KM, count_only=True)[0]) | |
| d_2km = int(ball_tree.query_radius(coord_rad, r=R_2KM, count_only=True)[0]) | |
| density_ratio = d_1km / (d_0p5 + 1) | |
| density_dist = d_1km * dist_km | |
| cluster_id = int(kmeans.predict([[lat, lon]])[0]) | |
| quarter = (month - 1) // 3 + 1 | |
| is_night = int(hour >= 20 or hour <= 5) | |
| row = { | |
| "Latitude": lat, | |
| "Longitude": lon, | |
| "dist_to_center_km": dist_km, | |
| "dist_sq": dist_sq, | |
| "density_0p5km": float(d_0p5), | |
| "density_1km": float(d_1km), | |
| "density_2km": float(d_2km), | |
| "density_ratio": density_ratio, | |
| "density_dist_interaction": density_dist, | |
| "lat_lon_product": lat_lon_prod, | |
| "cluster_id": float(cluster_id), | |
| "FIR_MONTH": float(month), | |
| "FIR_QUARTER": float(quarter), | |
| "FIR_IS_WEEKEND": float(is_weekend), | |
| "is_night": float(is_night), | |
| } | |
| df = pd.DataFrame([row]) | |
| for col in X_columns: | |
| if col not in df.columns: | |
| df[col] = 0.0 | |
| df = df[list(X_columns)].fillna(0.0) | |
| arr = df.to_numpy(dtype=np.float64) | |
| if arr.shape[1] < N_FEATURES: | |
| arr = np.hstack([arr, np.zeros((1, N_FEATURES - arr.shape[1]))]) | |
| elif arr.shape[1] > N_FEATURES: | |
| arr = arr[:, :N_FEATURES] | |
| return arr | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # INFERENCE | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def _safe_proba(model, X_scaled: np.ndarray, is_lgb: bool) -> Optional[np.ndarray]: | |
| try: | |
| name = type(model).__name__ | |
| if hasattr(model, "predict") and "Booster" in name: | |
| p = model.predict(X_scaled) | |
| return p[0] if p.ndim == 2 else p | |
| if is_lgb or "XGB" in name or "Forest" in name or "Trees" in name: | |
| return model.predict_proba(X_scaled)[0] | |
| if "LogisticRegression" in name: | |
| scores = model.decision_function(X_scaled)[0] | |
| e = np.exp(scores - scores.max()) | |
| return e / e.sum() | |
| return model.predict_proba(X_scaled)[0] | |
| except Exception as e: | |
| log.warning("Model %s failed: %s", type(model).__name__, e) | |
| return None | |
| def _predict_danger( | |
| lat: float, lon: float, | |
| hour: int = 12, month: int = 6, is_weekend: int = 0, | |
| ) -> dict: | |
| X_raw = _build_feature_row(lat, lon, hour, month, is_weekend) | |
| X_scaled = scaler.transform(X_raw) | |
| probas = [] | |
| for model, is_lgb, _ in _model_pool: | |
| p = _safe_proba(model, X_scaled, is_lgb) | |
| if p is not None and len(p) == 3: | |
| probas.append(p) | |
| if not probas: | |
| log.error("All models failed for (%.4f, %.4f)", lat, lon) | |
| proba = np.array([0.33, 0.34, 0.33]) | |
| elif len(probas) == 1: | |
| proba = probas[0] | |
| else: | |
| proba = np.mean(probas, axis=0) | |
| safe_p = float(proba[0]) | |
| high_p = float(proba[2]) | |
| # Weighted expected-value formula (v4.1) — avoids clustering at 50-70 | |
| med_p = max(0.0, 1.0 - safe_p - high_p) | |
| expected = safe_p * 100.0 + med_p * 50.0 # high_p contributes 0 | |
| raw = 100.0 * ((expected / 100.0) ** 0.55) | |
| base = int(max(0.0, min(100.0, raw))) | |
| penalty = _time_penalty(hour) | |
| score = max(5, min(100, base - penalty)) | |
| label, _ = _score_to_label(score) | |
| try: | |
| density_idx = list(X_columns).index("density_1km") | |
| density = int(X_raw[0, density_idx]) | |
| except (ValueError, IndexError): | |
| density = 0 | |
| return {"label": label, "score": score, "density": density, "hour": hour} | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # UTILITIES | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def _now_params(): | |
| n = datetime.datetime.now() | |
| return n.hour, n.month, int(n.weekday() >= 5) | |
| def _parse_time(t: str) -> int: | |
| t = (t or "Now").lower().strip() | |
| if t in ("now", ""): return datetime.datetime.now().hour | |
| if "night" in t: return 22 | |
| if "morning" in t: return 8 | |
| if "afternoon" in t: return 14 | |
| if "evening" in t: return 18 | |
| if "1 hour" in t: return (datetime.datetime.now().hour + 1) % 24 | |
| return datetime.datetime.now().hour | |
| def _osrm_leg(o_lat: float, o_lng: float, d_lat: float, d_lng: float) -> dict: | |
| if _HTTPX_OK: | |
| try: | |
| r = _httpx.get( | |
| f"http://router.project-osrm.org/route/v1/driving/" | |
| f"{o_lng},{o_lat};{d_lng},{d_lat}?overview=false", | |
| timeout=3.0, | |
| ) | |
| if r.status_code == 200: | |
| leg = r.json()["routes"][0]["legs"][0] | |
| return { | |
| "dist_km": round(leg["distance"] / 1000, 1), | |
| "mins": max(1, round(leg["duration"] / 60)), | |
| } | |
| except Exception: | |
| pass | |
| straight = geodesic((o_lat, o_lng), (d_lat, d_lng)).km | |
| return { | |
| "dist_km": round(straight * 1.3, 1), | |
| "mins": max(5, round(straight * 1.3 / 28 * 60)), | |
| } | |
| def _build_routes( | |
| o_lat: float, o_lng: float, | |
| d_lat: float, d_lng: float, | |
| hour: int, month: int, is_weekend: int, | |
| ) -> RouteResponse: | |
| m1_lat = (o_lat + d_lat) / 2 + 0.010 | |
| m1_lng = (o_lng + d_lng) / 2 + 0.010 | |
| m2_lat = (o_lat + d_lat) / 2 - 0.008 | |
| m2_lng = (o_lng + d_lng) / 2 - 0.008 | |
| with ThreadPoolExecutor(max_workers=10) as ex: | |
| f_main = ex.submit(_osrm_leg, o_lat, o_lng, d_lat, d_lng) | |
| f_a1 = ex.submit(_osrm_leg, o_lat, o_lng, m1_lat, m1_lng) | |
| f_b1 = ex.submit(_osrm_leg, m1_lat, m1_lng, d_lat, d_lng) | |
| f_a2 = ex.submit(_osrm_leg, o_lat, o_lng, m2_lat, m2_lng) | |
| f_b2 = ex.submit(_osrm_leg, m2_lat, m2_lng, d_lat, d_lng) | |
| f_orig = ex.submit(_predict_danger, o_lat, o_lng, hour, month, is_weekend) | |
| f_dest = ex.submit(_predict_danger, d_lat, d_lng, hour, month, is_weekend) | |
| f_mid = ex.submit(_predict_danger, (o_lat+d_lat)/2, (o_lng+d_lng)/2, hour, month, is_weekend) | |
| f_mid1 = ex.submit(_predict_danger, m1_lat, m1_lng, hour, month, is_weekend) | |
| f_mid2 = ex.submit(_predict_danger, m2_lat, m2_lng, hour, month, is_weekend) | |
| main = f_main.result() | |
| a1, b1 = f_a1.result(), f_b1.result() | |
| a2, b2 = f_a2.result(), f_b2.result() | |
| orig_r = f_orig.result() | |
| dest_r = f_dest.result() | |
| mid_r = f_mid.result() | |
| mid1_r = f_mid1.result() | |
| mid2_r = f_mid2.result() | |
| def ws(mid: dict) -> int: | |
| return int(orig_r["score"] * 0.2 + mid["score"] * 0.5 + dest_r["score"] * 0.3) | |
| routes_raw = [ | |
| { | |
| "name": "Main Route", | |
| "duration": f"{main['mins']} min", | |
| "distance": f"{main['dist_km']} km", | |
| "safety_score": ws(mid_r), | |
| "factors": _score_to_factors(ws(mid_r), mid_r["density"], hour), | |
| "is_recommended": False, | |
| }, | |
| { | |
| "name": "Alternate Route 1", | |
| "duration": f"{a1['mins'] + b1['mins']} min", | |
| "distance": f"{round(a1['dist_km'] + b1['dist_km'], 1)} km", | |
| "safety_score": ws(mid1_r), | |
| "factors": _score_to_factors(ws(mid1_r), mid1_r["density"], hour), | |
| "is_recommended": False, | |
| }, | |
| { | |
| "name": "Alternate Route 2", | |
| "duration": f"{a2['mins'] + b2['mins']} min", | |
| "distance": f"{round(a2['dist_km'] + b2['dist_km'], 1)} km", | |
| "safety_score": ws(mid2_r), | |
| "factors": _score_to_factors(ws(mid2_r), mid2_r["density"], hour), | |
| "is_recommended": False, | |
| }, | |
| ] | |
| routes_raw.sort(key=lambda r: r["safety_score"], reverse=True) | |
| routes_raw[0]["is_recommended"] = True | |
| overall = int(sum(r["safety_score"] for r in routes_raw) / len(routes_raw)) | |
| return RouteResponse( | |
| overall_score=overall, | |
| routes=[RouteResult(**r) for r in routes_raw], | |
| ) | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # ENDPOINTS | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "version": "4.1", | |
| "models": [m[2] for m in _model_pool], | |
| "ensemble": ENSEMBLE_OK, | |
| "n_features": N_FEATURES, | |
| "n_clusters": kmeans.n_clusters, | |
| "feature_cols": list(X_columns), | |
| "scoring": "weighted_expected_v4.1", | |
| } | |
| # GET — used by heatmap_service.dart SafetyApiService.predictArea() | |
| def predict_area_get(latitude: float, longitude: float): | |
| try: | |
| hour, month, iw = _now_params() | |
| r = _predict_danger(latitude, longitude, hour, month, iw) | |
| label, level = _score_to_label(r["score"]) | |
| return AreaResponse( | |
| safety_score=r["score"], | |
| danger_level=level, | |
| danger_label=label, | |
| factors=_score_to_factors(r["score"], r["density"], hour), | |
| ) | |
| except Exception as e: | |
| log.exception("predict_area_get failed") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # POST — used by safety_api_service.dart SafetyApiService.predictArea() | |
| def predict_area_post(req: AreaRequest): | |
| try: | |
| h, mo, iw = _now_params() | |
| hour = req.hour if req.hour is not None else h | |
| month = req.month if req.month is not None else mo | |
| is_weekend = req.is_weekend if req.is_weekend is not None else iw | |
| r = _predict_danger(req.latitude, req.longitude, hour, month, is_weekend) | |
| label, level = _score_to_label(r["score"]) | |
| return AreaResponse( | |
| safety_score=r["score"], | |
| danger_level=level, | |
| danger_label=label, | |
| factors=_score_to_factors(r["score"], r["density"], hour), | |
| ) | |
| except Exception as e: | |
| log.exception("predict_area_post failed") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # Alias used by some older callers | |
| def predict_area_full(req: AreaRequest): | |
| return predict_area_post(req) | |
| # GET — used by heatmap_service.dart SafetyApiService.analyzeRoute() | |
| def analyze_route_get( | |
| origin_lat: float, origin_lng: float, | |
| dest_lat: float, dest_lng: float, | |
| time: str = "Now", | |
| ): | |
| try: | |
| hour = _parse_time(time) | |
| _, month, iw = _now_params() | |
| return _build_routes(origin_lat, origin_lng, dest_lat, dest_lng, | |
| hour, month, iw) | |
| except Exception as e: | |
| log.exception("analyze_route_get failed") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # POST — used by safety_api_service.dart SafetyApiService.analyzeRoute() | |
| def analyze_route_post(req: RouteRequest): | |
| try: | |
| hour = _parse_time(req.time) | |
| _, month, iw = _now_params() | |
| return _build_routes(req.origin_lat, req.origin_lng, | |
| req.dest_lat, req.dest_lng, | |
| hour, month, iw) | |
| except Exception as e: | |
| log.exception("analyze_route_post failed") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # GET — used by heatmap_service.dart HeatmapService.fetchHeatmap() | |
| def heatmap_data( | |
| latitude: float = 12.9716, | |
| longitude: float = 77.5946, | |
| radius_km: float = 2.0, | |
| step_km: float = 0.3, | |
| ): | |
| """ | |
| risk field MUST be "Low", "Medium", or "High" — | |
| heatmap_screen.dart _riskFill() switches on these exact strings. | |
| score is used for circle radius: (100-score)/100 * 60 + 80. | |
| """ | |
| try: | |
| hour, month, iw = _now_params() | |
| lat_step = step_km / 111.0 | |
| lng_step = step_km / (111.0 * max(abs(math.cos(math.radians(latitude))), 0.01)) | |
| lat_range = np.arange( | |
| latitude - radius_km / 111.0, | |
| latitude + radius_km / 111.0, | |
| lat_step, | |
| ) | |
| lng_range = np.arange( | |
| longitude - radius_km / 111.0, | |
| longitude + radius_km / 111.0, | |
| lng_step, | |
| ) | |
| MAX_PTS = 200 | |
| if len(lat_range) * len(lng_range) > MAX_PTS: | |
| s = max(1, int(math.ceil(math.sqrt( | |
| len(lat_range) * len(lng_range) / MAX_PTS)))) | |
| lat_range = lat_range[::s] | |
| lng_range = lng_range[::s] | |
| grid = [(float(la), float(lo)) for la in lat_range for lo in lng_range] | |
| log.info("Heatmap grid: %d points", len(grid)) | |
| results_map: dict = {} | |
| with ThreadPoolExecutor(max_workers=24) as ex: | |
| futures = { | |
| ex.submit(_predict_danger, la, lo, hour, month, iw): (la, lo) | |
| for la, lo in grid | |
| } | |
| for fut in as_completed(futures): | |
| coord = futures[fut] | |
| try: | |
| results_map[coord] = fut.result() | |
| except Exception as exc: | |
| log.warning("Heatmap point %s skipped: %s", coord, exc) | |
| points = [] | |
| for la, lo in grid: | |
| res = results_map.get((la, lo)) | |
| if res is None: | |
| continue | |
| _lbl, level = _score_to_label(res["score"]) | |
| points.append(HeatmapPoint( | |
| lat=round(la, 5), | |
| lng=round(lo, 5), | |
| risk=level, | |
| score=res["score"], | |
| )) | |
| if not points: | |
| raise HTTPException(status_code=500, | |
| detail="No valid predictions returned.") | |
| return HeatmapResponse( | |
| points=points, | |
| center_lat=latitude, | |
| center_lng=longitude, | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| log.exception("heatmap_data failed") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # POST — chatbot_ui.dart getBotResponse() POSTs to /ai | |
| async def ai_chat(body: AiRequest): | |
| try: | |
| msg = body.message.strip() | |
| lower = msg.lower() | |
| if not msg: | |
| return AiResponse(reply="Please enter a message.") | |
| FAST = [ | |
| ({"danger", "emergency", "attack", "unsafe", "threat", "help"}, | |
| "Stay calm. Call 112 immediately and move to a safe, crowded place."), | |
| ({"police"}, | |
| "Call 112 for police. Use the SOS button in the app for instant alerts."), | |
| ({"sos", "panic", "alert"}, | |
| "Press the SOS button to instantly alert your emergency contacts."), | |
| ({"hospital", "ambulance", "injured", "hurt", "accident", "medical"}, | |
| "Call 108 for an ambulance. Tap Nearby Help for the nearest hospital."), | |
| ({"route", "safe route", "path", "navigate"}, | |
| "Tap Plan Safe Route on the home screen for ML-rated safe routes."), | |
| ({"heatmap", "crime map", "danger zone"}, | |
| "Check the Safety Heatmap on the home screen for real-time crime density."), | |
| ({"safe", "area", "location", "nearby"}, | |
| "Use the heatmap on the home screen for a live area safety overview."), | |
| ] | |
| for kws, reply in FAST: | |
| if any(k in lower for k in kws): | |
| return AiResponse(reply=reply) | |
| if groq_client is None: | |
| return AiResponse( | |
| reply="I am your Nyvra safety assistant. For emergencies, call 112. " | |
| "Use the SOS button to alert your contacts instantly." | |
| ) | |
| import asyncio | |
| loop = asyncio.get_running_loop() | |
| resp = await loop.run_in_executor( | |
| None, | |
| lambda: groq_client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| max_tokens=120, | |
| temperature=0.4, | |
| messages=[ | |
| {"role": "system", "content": ( | |
| "You are Nyvra, a concise women's safety assistant for India. " | |
| "Reply in 2-3 short sentences. Be calm, practical, empathetic. " | |
| "For emergencies always say: call 112." | |
| )}, | |
| {"role": "user", "content": msg}, | |
| ], | |
| ) | |
| ) | |
| return AiResponse(reply=resp.choices[0].message.content.strip()) | |
| except Exception as e: | |
| log.exception("ai_chat failed") | |
| return AiResponse( | |
| reply="I am here to help with your safety. For emergencies, call 112 immediately." | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=8000, reload=False) |