Spaces:
Running
Running
fix: unconditional SPA catch-all route + diagnostic fallback for missing frontend build
Browse files- backend/main.py +32 -26
backend/main.py
CHANGED
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
| 5 |
import pandas as pd
|
| 6 |
from fastapi import FastAPI, HTTPException
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
-
from fastapi.responses import FileResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
from src.app.schemas import (
|
|
@@ -29,10 +29,11 @@ def _safe_int(val) -> int | None:
|
|
| 29 |
except Exception:
|
| 30 |
return None
|
| 31 |
|
|
|
|
| 32 |
app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
|
| 33 |
app.add_middleware(
|
| 34 |
CORSMiddleware,
|
| 35 |
-
|
| 36 |
allow_credentials=True,
|
| 37 |
allow_methods=["*"],
|
| 38 |
allow_headers=["*"],
|
|
@@ -67,11 +68,9 @@ def get_samples(track: str = "gcc"):
|
|
| 67 |
df = df[df["dataset_track"].fillna("us") == track]
|
| 68 |
if df.empty:
|
| 69 |
raise HTTPException(status_code=404, detail=f"No samples found for dataset track: {track}")
|
| 70 |
-
|
| 71 |
if "Start_Time" in df.columns:
|
| 72 |
df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
|
| 73 |
df = df.sort_values(by="Start_Time", ascending=False)
|
| 74 |
-
|
| 75 |
# Pick one representative sample per severity level for a compact, diverse preview
|
| 76 |
sev_map_int = {1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
|
| 77 |
severity_order = [3, 2, 4, 1] # High, Medium, Critical, Low β most interesting first
|
|
@@ -94,19 +93,15 @@ def get_samples(track: str = "gcc"):
|
|
| 94 |
selected_rows.append(row)
|
| 95 |
used_indices.add(row.name)
|
| 96 |
sample_df = pd.DataFrame(selected_rows)
|
| 97 |
-
|
| 98 |
items = []
|
| 99 |
for idx, row in sample_df.iterrows():
|
| 100 |
def clean(val):
|
| 101 |
s = str(val).strip() if pd.notna(val) else ""
|
| 102 |
return "" if s.lower() in ("nan", "none", "") else s
|
| 103 |
-
|
| 104 |
loc_cols = ["road_name", "Street", "district", "City", "State", "emirate"]
|
| 105 |
location_parts = [clean(row.get(col)) for col in loc_cols]
|
| 106 |
title = " Β· ".join([p for p in location_parts if p][:3]) or f"Sample incident {idx + 1}"
|
| 107 |
-
|
| 108 |
desc = clean(row.get("Description", "")) or "No description available."
|
| 109 |
-
|
| 110 |
sev = row.get("Severity", "")
|
| 111 |
if clean(str(sev)):
|
| 112 |
if "severity" not in desc.lower():
|
|
@@ -116,11 +111,9 @@ def get_samples(track: str = "gcc"):
|
|
| 116 |
desc = f"{desc} Classified as {sev_str} severity."
|
| 117 |
except Exception:
|
| 118 |
pass
|
| 119 |
-
|
| 120 |
src_lbl = clean(row.get("source_label", ""))
|
| 121 |
if not src_lbl:
|
| 122 |
src_lbl = "US Accidents" if track == "us" else "GCC sample"
|
| 123 |
-
|
| 124 |
items.append(
|
| 125 |
SampleItem(
|
| 126 |
id=str(idx + 1),
|
|
@@ -133,7 +126,6 @@ def get_samples(track: str = "gcc"):
|
|
| 133 |
return SamplesResponse(items=items)
|
| 134 |
|
| 135 |
|
| 136 |
-
|
| 137 |
@app.post("/summarize", response_model=SummarizeResponse)
|
| 138 |
def summarize(request: SummarizeRequest):
|
| 139 |
try:
|
|
@@ -163,20 +155,34 @@ def compare(request: CompareRequest):
|
|
| 163 |
except Exception as exc:
|
| 164 |
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 165 |
|
|
|
|
| 166 |
# ββ Serve React Frontend (Single-Container Deployment e.g., Hugging Face) ββ
|
| 167 |
_DIST_PATH = Path(__file__).parent.parent / "frontend" / "dist"
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import pandas as pd
|
| 6 |
from fastapi import FastAPI, HTTPException
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
from src.app.schemas import (
|
|
|
|
| 29 |
except Exception:
|
| 30 |
return None
|
| 31 |
|
| 32 |
+
|
| 33 |
app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
|
| 34 |
app.add_middleware(
|
| 35 |
CORSMiddleware,
|
| 36 |
+
allow_origins=["*"],
|
| 37 |
allow_credentials=True,
|
| 38 |
allow_methods=["*"],
|
| 39 |
allow_headers=["*"],
|
|
|
|
| 68 |
df = df[df["dataset_track"].fillna("us") == track]
|
| 69 |
if df.empty:
|
| 70 |
raise HTTPException(status_code=404, detail=f"No samples found for dataset track: {track}")
|
|
|
|
| 71 |
if "Start_Time" in df.columns:
|
| 72 |
df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
|
| 73 |
df = df.sort_values(by="Start_Time", ascending=False)
|
|
|
|
| 74 |
# Pick one representative sample per severity level for a compact, diverse preview
|
| 75 |
sev_map_int = {1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
|
| 76 |
severity_order = [3, 2, 4, 1] # High, Medium, Critical, Low β most interesting first
|
|
|
|
| 93 |
selected_rows.append(row)
|
| 94 |
used_indices.add(row.name)
|
| 95 |
sample_df = pd.DataFrame(selected_rows)
|
|
|
|
| 96 |
items = []
|
| 97 |
for idx, row in sample_df.iterrows():
|
| 98 |
def clean(val):
|
| 99 |
s = str(val).strip() if pd.notna(val) else ""
|
| 100 |
return "" if s.lower() in ("nan", "none", "") else s
|
|
|
|
| 101 |
loc_cols = ["road_name", "Street", "district", "City", "State", "emirate"]
|
| 102 |
location_parts = [clean(row.get(col)) for col in loc_cols]
|
| 103 |
title = " Β· ".join([p for p in location_parts if p][:3]) or f"Sample incident {idx + 1}"
|
|
|
|
| 104 |
desc = clean(row.get("Description", "")) or "No description available."
|
|
|
|
| 105 |
sev = row.get("Severity", "")
|
| 106 |
if clean(str(sev)):
|
| 107 |
if "severity" not in desc.lower():
|
|
|
|
| 111 |
desc = f"{desc} Classified as {sev_str} severity."
|
| 112 |
except Exception:
|
| 113 |
pass
|
|
|
|
| 114 |
src_lbl = clean(row.get("source_label", ""))
|
| 115 |
if not src_lbl:
|
| 116 |
src_lbl = "US Accidents" if track == "us" else "GCC sample"
|
|
|
|
| 117 |
items.append(
|
| 118 |
SampleItem(
|
| 119 |
id=str(idx + 1),
|
|
|
|
| 126 |
return SamplesResponse(items=items)
|
| 127 |
|
| 128 |
|
|
|
|
| 129 |
@app.post("/summarize", response_model=SummarizeResponse)
|
| 130 |
def summarize(request: SummarizeRequest):
|
| 131 |
try:
|
|
|
|
| 155 |
except Exception as exc:
|
| 156 |
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 157 |
|
| 158 |
+
|
| 159 |
# ββ Serve React Frontend (Single-Container Deployment e.g., Hugging Face) ββ
|
| 160 |
_DIST_PATH = Path(__file__).parent.parent / "frontend" / "dist"
|
| 161 |
+
_INDEX_HTML = _DIST_PATH / "index.html"
|
| 162 |
+
_ASSETS_PATH = _DIST_PATH / "assets"
|
| 163 |
+
|
| 164 |
+
# Mount static assets if the build exists
|
| 165 |
+
if _ASSETS_PATH.exists() and _ASSETS_PATH.is_dir():
|
| 166 |
+
app.mount("/assets", StaticFiles(directory=str(_ASSETS_PATH)), name="assets")
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# Catch-all SPA route β always registered so HF Spaces never gets a 404 at root
|
| 170 |
+
@app.get("/{full_path:path}")
|
| 171 |
+
async def serve_frontend(full_path: str):
|
| 172 |
+
# Try to serve an exact file from dist (e.g. favicon.ico, robots.txt)
|
| 173 |
+
req_path = _DIST_PATH / full_path
|
| 174 |
+
if req_path.exists() and req_path.is_file():
|
| 175 |
+
return FileResponse(req_path)
|
| 176 |
+
# SPA fallback: always return index.html so React Router handles the path
|
| 177 |
+
if _INDEX_HTML.exists():
|
| 178 |
+
return FileResponse(_INDEX_HTML)
|
| 179 |
+
# Frontend build not found β return diagnostic JSON instead of 404
|
| 180 |
+
return JSONResponse(
|
| 181 |
+
status_code=200,
|
| 182 |
+
content={
|
| 183 |
+
"status": "api_only",
|
| 184 |
+
"message": "Frontend build not found. API is running.",
|
| 185 |
+
"api_docs": "/docs",
|
| 186 |
+
"health": "/health",
|
| 187 |
+
},
|
| 188 |
+
)
|