MohdUmar0223 commited on
Commit
677ac5f
·
verified ·
1 Parent(s): d0d1df7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -124
app.py CHANGED
@@ -3,16 +3,6 @@ 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
- 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
@@ -44,24 +34,16 @@ from datetime import datetime, timezone
44
  try:
45
  import spaces
46
  except ImportError:
47
- spaces = None
 
 
 
 
 
 
 
 
48
 
49
- def gpu_decorator(func=None, duration=None):
50
- """ZeroGPU decorator wrapper that applies spaces.GPU on HF or identity on CPU."""
51
- if spaces is not None:
52
- try:
53
- if func is None:
54
- return spaces.GPU(duration=duration) if duration else spaces.GPU
55
- if callable(func):
56
- return spaces.GPU(func)
57
- return spaces.GPU
58
- except Exception:
59
- pass
60
- if func is None:
61
- return lambda f: f
62
- if callable(func):
63
- return func
64
- return lambda f: f
65
 
66
  from config import get_device, HAS_SPACES
67
  from services.forecast.service import forecast_aqi
@@ -79,7 +61,7 @@ async def _async_handle_forecast_ui(aqi_text_input: str, lat_val: str, lon_val:
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]
@@ -116,9 +98,9 @@ async def _async_handle_forecast_ui(aqi_text_input: str, lat_val: str, lon_val:
116
  return f"❌ Execution Failed: {str(e)}", None, ""
117
 
118
 
119
- @gpu_decorator
120
  def handle_forecast_ui(aqi_text_input: str, lat_val: str, lon_val: str):
121
- """Gradio handler for AQI Forecasting wrapped with @gpu_decorator."""
122
  return asyncio.run(_async_handle_forecast_ui(aqi_text_input, lat_val, lon_val))
123
 
124
 
@@ -158,9 +140,9 @@ async def _async_handle_vision_ui(input_image):
158
  return f"❌ Analysis Failed: {str(e)}", ""
159
 
160
 
161
- @gpu_decorator
162
  def handle_vision_ui(input_image):
163
- """Gradio handler for Satellite Vision wrapped with @gpu_decorator."""
164
  return asyncio.run(_async_handle_vision_ui(input_image))
165
 
166
 
@@ -229,108 +211,93 @@ with gr.Blocks(title="AQI Intelligence Engine — HF ZeroGPU Edition", theme=gr.
229
  f"""
230
  ### 🖥️ Active Deployment Info
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=["*"],
263
- allow_credentials=True,
264
- allow_methods=["*"],
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")
277
- res = await forecast_aqi(aqi_hist, lat=lat, lon=lon)
278
- return JSONResponse(res)
279
- except Exception as e:
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)
293
- if "," in base64_str:
294
- base64_str = base64_str.split(",", 1)[1]
295
- img_bytes = base64.b64decode(base64_str)
296
- pil_img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
297
- res = await detect_pollution_sources(pil_img)
298
- return JSONResponse(res)
299
- except Exception as e:
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",
309
- "device": get_device(),
310
- "timestamp": datetime.now(timezone.utc).isoformat(),
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)))
 
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
 
34
  try:
35
  import spaces
36
  except ImportError:
37
+ class spaces:
38
+ _is_mock = True
39
+ @staticmethod
40
+ def GPU(func_or_duration=None, **kwargs):
41
+ if callable(func_or_duration):
42
+ return func_or_duration
43
+ def decorator(func):
44
+ return func
45
+ return decorator
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  from config import get_device, HAS_SPACES
49
  from services.forecast.service import forecast_aqi
 
61
  try:
62
  if not aqi_text_input or not aqi_text_input.strip():
63
  return "❌ Error: Please provide at least 24 hourly AQI readings.", None, ""
64
+
65
  # Parse CSV string into list of floats
66
  raw_vals = [x.strip() for x in aqi_text_input.split(",") if x.strip()]
67
  aqi_series = [float(x) for x in raw_vals]
 
98
  return f"❌ Execution Failed: {str(e)}", None, ""
99
 
100
 
101
+ @spaces.GPU
102
  def handle_forecast_ui(aqi_text_input: str, lat_val: str, lon_val: str):
103
+ """Gradio handler for AQI Forecasting."""
104
  return asyncio.run(_async_handle_forecast_ui(aqi_text_input, lat_val, lon_val))
105
 
106
 
 
140
  return f"❌ Analysis Failed: {str(e)}", ""
141
 
142
 
143
+ @spaces.GPU
144
  def handle_vision_ui(input_image):
145
+ """Gradio handler for Satellite Vision."""
146
  return asyncio.run(_async_handle_vision_ui(input_image))
147
 
148
 
 
211
  f"""
212
  ### 🖥️ Active Deployment Info
213
  - **Primary Compute Hardware**: `{device_active}`
214
+ - **HuggingFace `spaces` SDK Available**: `{not getattr(spaces, "_is_mock", False)}`
215
+ - **ZeroGPU Status**: `{"Active & Ready for dynamic GPU acceleration" if not getattr(spaces, "_is_mock", False) else "Running on standard CPU mode (Universal Compatibility)"}`
216
+
217
  ### 🔌 REST API Endpoints
218
+ When deployed on Hugging Face Spaces, you can query REST endpoints directly:
219
+ - `POST /forecast`: Direct historical AQI prediction payload
220
+ - `POST /analyze-image`: Satellite base64 image breakdown
221
+ - `GET /health`: Core server state & device telemetry
222
  """
223
  )
224
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  from starlette.responses import JSONResponse
226
  from starlette.requests import Request
227
 
228
+ # Initialize the Gradio queue — this creates the underlying FastAPI app object
229
+ # MUST be called before registering REST routes on demo.app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  demo.queue()
231
 
 
 
 
 
232
 
233
+ def register_api_routes(gradio_app):
234
+ """Register custom REST API endpoints on the Gradio FastAPI app."""
235
+
236
+ @gradio_app.post("/api/v1/forecast")
237
+ async def api_forecast(request: Request):
238
+ """REST API endpoint for historical AQI prediction."""
239
+ try:
240
+ payload = await request.json()
241
+ from services.forecast.service import forecast_aqi
242
+ aqi_hist = payload.get("aqi_history", [])
243
+ lat = payload.get("lat")
244
+ lon = payload.get("lon")
245
+ res = await forecast_aqi(aqi_hist, lat=lat, lon=lon)
246
+ return JSONResponse(res)
247
+ except Exception as e:
248
+ logger.error(f"API Forecast error: {e}")
249
+ return JSONResponse({"error": str(e)}, status_code=400)
250
+
251
+ @gradio_app.post("/api/v1/analyze-image")
252
+ async def api_analyze_image(request: Request):
253
+ """REST API endpoint for satellite image pollution breakdown."""
254
+ try:
255
+ payload = await request.json()
256
+ import base64
257
+ from services.vision.service import detect_pollution_sources
258
+ base64_str = payload.get("image_base64", "")
259
+ if not base64_str:
260
+ return JSONResponse({"error": "No image_base64 provided"}, status_code=400)
261
+ if "," in base64_str:
262
+ base64_str = base64_str.split(",", 1)[1]
263
+ img_bytes = base64.b64decode(base64_str)
264
+ pil_img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
265
+ res = await detect_pollution_sources(pil_img)
266
+ return JSONResponse(res)
267
+ except Exception as e:
268
+ logger.error(f"API Vision error: {e}")
269
+ return JSONResponse({"error": str(e)}, status_code=400)
270
+
271
+ @gradio_app.get("/api/v1/health")
272
+ async def api_health(request: Request):
273
+ """Health check endpoint."""
274
+ return JSONResponse({
275
+ "status": "ok",
276
+ "device": get_device(),
277
+ "timestamp": datetime.now(timezone.utc).isoformat(),
278
+ "version": "2.0.0",
279
+ })
280
+
281
+
282
+ # Register REST API routes on the Gradio underlying FastAPI app
283
+ try:
284
+ from fastapi.middleware.cors import CORSMiddleware
285
+ # Explicitly configure CORS on the underlying FastAPI app to allow direct requests from the browser
286
+ demo.app.add_middleware(
287
+ CORSMiddleware,
288
+ allow_origins=["*"],
289
+ allow_credentials=True,
290
+ allow_methods=["*"],
291
+ allow_headers=["*"],
292
+ )
293
+ register_api_routes(demo.app)
294
+ logger.info("CORS middleware and REST API routes registered successfully on Gradio FastAPI app.")
295
+ except Exception as e:
296
+ logger.warning(f"Could not register REST API routes or CORS (will be available via Gradio only): {e}")
297
 
298
+ # Export Gradio demo as root app object for Hugging Face ZeroGPU SDK
299
+ app = demo
300
 
301
  if __name__ == "__main__":
302
+ # Local development only — on HF Spaces, uvicorn runs the app directly
303
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)