MohdUmar0223 commited on
Commit
4ad561b
Β·
verified Β·
1 Parent(s): 4faa043

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +48 -20
app.py CHANGED
@@ -3,6 +3,16 @@ AQI Intelligence Engine β€” Hugging Face Gradio + ZeroGPU Main Application Entry
3
 
4
  Supports both standalone Gradio Web Interface and REST API backend.
5
  Runs seamlessly on Hugging Face ZeroGPU spaces as well as standard CPU fallback.
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  import os
@@ -69,7 +79,7 @@ async def _async_handle_forecast_ui(aqi_text_input: str, lat_val: str, lon_val:
69
  try:
70
  if not aqi_text_input or not aqi_text_input.strip():
71
  return "❌ Error: Please provide at least 24 hourly AQI readings.", None, ""
72
-
73
  # Parse CSV string into list of floats
74
  raw_vals = [x.strip() for x in aqi_text_input.split(",") if x.strip()]
75
  aqi_series = [float(x) for x in raw_vals]
@@ -221,24 +231,32 @@ with gr.Blocks(title="AQI Intelligence Engine β€” HF ZeroGPU Edition", theme=gr.
221
  - **Primary Compute Hardware**: `{device_active}`
222
  - **HuggingFace `spaces` SDK Available**: `{spaces is not None}`
223
  - **ZeroGPU Status**: `{"Active & Ready for dynamic GPU acceleration" if spaces is not None else "Running on standard CPU mode (Universal Compatibility)"}`
224
-
225
  ### πŸ”Œ REST API Endpoints
226
- When deployed on Hugging Face Spaces, you can query REST endpoints directly:
227
- - `POST /forecast`: Direct historical AQI prediction payload
228
- - `POST /analyze-image`: Satellite base64 image breakdown
229
- - `GET /health`: Core server state & device telemetry
230
  """
231
  )
232
 
 
 
 
 
 
 
 
 
 
 
233
  from fastapi import FastAPI
234
  from fastapi.middleware.cors import CORSMiddleware
235
  from starlette.responses import JSONResponse
236
  from starlette.requests import Request
237
 
238
- # Create the FastAPI app
239
- app = FastAPI(title="AQI Intelligence Engine API")
240
 
241
- # Configure CORS directly on the FastAPI app
242
  app.add_middleware(
243
  CORSMiddleware,
244
  allow_origins=["*"],
@@ -247,14 +265,12 @@ app.add_middleware(
247
  allow_headers=["*"],
248
  )
249
 
250
- # Register REST API routes on the FastAPI app
251
- @app.post("/api/forecast")
252
  @app.post("/api/v1/forecast")
253
  async def api_forecast(request: Request):
254
  """REST API endpoint for historical AQI prediction."""
255
  try:
256
  payload = await request.json()
257
- from services.forecast.service import forecast_aqi
258
  aqi_hist = payload.get("aqi_history", [])
259
  lat = payload.get("lat")
260
  lon = payload.get("lon")
@@ -264,14 +280,13 @@ async def api_forecast(request: Request):
264
  logger.error(f"API Forecast error: {e}")
265
  return JSONResponse({"error": str(e)}, status_code=400)
266
 
267
- @app.post("/api/analyze-image")
268
  @app.post("/api/v1/analyze-image")
269
  async def api_analyze_image(request: Request):
270
  """REST API endpoint for satellite image pollution breakdown."""
271
  try:
272
  payload = await request.json()
273
  import base64
274
- from services.vision.service import detect_pollution_sources
275
  base64_str = payload.get("image_base64", "")
276
  if not base64_str:
277
  return JSONResponse({"error": "No image_base64 provided"}, status_code=400)
@@ -285,9 +300,9 @@ async def api_analyze_image(request: Request):
285
  logger.error(f"API Vision error: {e}")
286
  return JSONResponse({"error": str(e)}, status_code=400)
287
 
288
- @app.get("/api/health")
289
  @app.get("/api/v1/health")
290
- async def api_health(request: Request):
291
  """Health check endpoint."""
292
  return JSONResponse({
293
  "status": "ok",
@@ -296,13 +311,26 @@ async def api_health(request: Request):
296
  "version": "2.0.0",
297
  })
298
 
299
- # Initialize the Gradio queue β€” this creates the underlying FastAPI app object
 
 
 
 
300
  demo.queue()
301
 
302
- # Mount the Gradio app onto the FastAPI app at root path
 
 
303
  app = gr.mount_gradio_app(app, demo, path="/")
304
 
 
 
 
 
 
305
  if __name__ == "__main__":
306
  import uvicorn
307
- # Local development only β€” on HF Spaces, uvicorn runs the app directly
308
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
3
 
4
  Supports both standalone Gradio Web Interface and REST API backend.
5
  Runs seamlessly on Hugging Face ZeroGPU spaces as well as standard CPU fallback.
6
+
7
+ FIX (2026-07-15): Custom REST routes (/api/v1/forecast, /api/v1/analyze-image,
8
+ /api/v1/health) were previously registered on `demo.app`, which does not exist
9
+ yet at import time (Gradio only builds it inside `.launch()`). That meant the
10
+ routes were silently never attached, and `demo.launch()` built a brand-new
11
+ FastAPI app internally that had never seen them β€” hence 404/405 in production.
12
+
13
+ Fix: build our own `FastAPI()` instance up front, register the routes on it,
14
+ mount the Gradio Blocks app into it with `gr.mount_gradio_app`, and serve that
15
+ combined app directly with uvicorn instead of calling `demo.launch()`.
16
  """
17
 
18
  import os
 
79
  try:
80
  if not aqi_text_input or not aqi_text_input.strip():
81
  return "❌ Error: Please provide at least 24 hourly AQI readings.", None, ""
82
+
83
  # Parse CSV string into list of floats
84
  raw_vals = [x.strip() for x in aqi_text_input.split(",") if x.strip()]
85
  aqi_series = [float(x) for x in raw_vals]
 
231
  - **Primary Compute Hardware**: `{device_active}`
232
  - **HuggingFace `spaces` SDK Available**: `{spaces is not None}`
233
  - **ZeroGPU Status**: `{"Active & Ready for dynamic GPU acceleration" if spaces is not None else "Running on standard CPU mode (Universal Compatibility)"}`
234
+
235
  ### πŸ”Œ REST API Endpoints
236
+ Query REST endpoints directly at this Space's root URL:
237
+ - `POST /api/v1/forecast`: Direct historical AQI prediction payload
238
+ - `POST /api/v1/analyze-image`: Satellite base64 image breakdown
239
+ - `GET /api/v1/health`: Core server state & device telemetry
240
  """
241
  )
242
 
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # REST API β€” built on our OWN FastAPI instance, not on `demo.app`.
246
+ #
247
+ # `demo.app` does not exist until Gradio's `.launch()` builds it internally,
248
+ # so registering routes on it here (before launch) is a no-op that silently
249
+ # fails. Instead we own the FastAPI app from the start and mount Gradio's UI
250
+ # into it, so there is exactly one app object and it has everything on it.
251
+ # ---------------------------------------------------------------------------
252
+
253
  from fastapi import FastAPI
254
  from fastapi.middleware.cors import CORSMiddleware
255
  from starlette.responses import JSONResponse
256
  from starlette.requests import Request
257
 
258
+ app = FastAPI()
 
259
 
 
260
  app.add_middleware(
261
  CORSMiddleware,
262
  allow_origins=["*"],
 
265
  allow_headers=["*"],
266
  )
267
 
268
+
 
269
  @app.post("/api/v1/forecast")
270
  async def api_forecast(request: Request):
271
  """REST API endpoint for historical AQI prediction."""
272
  try:
273
  payload = await request.json()
 
274
  aqi_hist = payload.get("aqi_history", [])
275
  lat = payload.get("lat")
276
  lon = payload.get("lon")
 
280
  logger.error(f"API Forecast error: {e}")
281
  return JSONResponse({"error": str(e)}, status_code=400)
282
 
283
+
284
  @app.post("/api/v1/analyze-image")
285
  async def api_analyze_image(request: Request):
286
  """REST API endpoint for satellite image pollution breakdown."""
287
  try:
288
  payload = await request.json()
289
  import base64
 
290
  base64_str = payload.get("image_base64", "")
291
  if not base64_str:
292
  return JSONResponse({"error": "No image_base64 provided"}, status_code=400)
 
300
  logger.error(f"API Vision error: {e}")
301
  return JSONResponse({"error": str(e)}, status_code=400)
302
 
303
+
304
  @app.get("/api/v1/health")
305
+ async def api_health():
306
  """Health check endpoint."""
307
  return JSONResponse({
308
  "status": "ok",
 
311
  "version": "2.0.0",
312
  })
313
 
314
+
315
+ logger.info("Custom REST API routes registered: /api/v1/forecast, /api/v1/analyze-image, /api/v1/health")
316
+
317
+ # Enable Gradio's internal event queue (needed for streaming/progress updates
318
+ # in the Blocks UI) before mounting.
319
  demo.queue()
320
 
321
+ # Mount the Gradio Blocks UI onto our FastAPI app at root ("/"). From this
322
+ # point on, `app` is the single combined ASGI application β€” it carries both
323
+ # the Gradio UI and the custom /api/v1/* routes above.
324
  app = gr.mount_gradio_app(app, demo, path="/")
325
 
326
+ # Log every route at startup so a misconfiguration is visible immediately in
327
+ # the Space logs rather than discovered later via a 404 in the browser.
328
+ logger.info("Registered routes: %s", [getattr(r, "path", r) for r in app.routes])
329
+
330
+
331
  if __name__ == "__main__":
332
  import uvicorn
333
+ # Serve our combined `app` object directly. Do NOT call `demo.launch()`
334
+ # here β€” that would build a second, separate FastAPI app internally that
335
+ # never saw the routes registered above, which was the original bug.
336
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))