mahwizzzz commited on
Commit
d3bf2db
·
verified ·
1 Parent(s): a4adc7d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -191
app.py CHANGED
@@ -6,49 +6,48 @@ import plotly.graph_objects as go
6
  import requests
7
  import os
8
  from datetime import datetime, timezone, timedelta
9
- from anthropic import Anthropic
10
  from typing import List, Optional
11
 
12
  # --- Configuration & Model Loading ---
13
- MODEL_ID = "mahwizzzz/skyloom"
14
- CONTEXT_LEN = 1024
15
- MAX_HORIZON = 72
16
 
17
- print("Loading TimesFM model …")
18
- device = "cuda" if torch.cuda.is_available() else "cpu"
19
- from transformers import TimesFm2_5ModelForPrediction
20
 
21
- # Load model with float32 for CPU optimization
22
- model = TimesFm2_5ModelForPrediction.from_pretrained(
23
- MODEL_ID,
24
  torch_dtype=torch.float32,
25
- device_map=device if device == "cuda" else None,
 
 
 
26
  )
27
 
28
- if device == "cpu":
29
- print("Quantizing model for CPU inference...")
30
- # Dynamic quantization for linear layers to improve CPU latency and reduce memory
31
- model = torch.quantization.quantize_dynamic(
32
- model, {torch.nn.Linear}, dtype=torch.qint8
33
- )
34
- print("Model quantized for CPU.")
35
-
36
- model.eval()
37
- print(f"Model ready on {device}")
 
38
 
39
- anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY", ""))
40
 
41
  # --- AQI Logic ---
42
  def pm25_to_aqi(pm25: float) -> float:
43
  pm25 = max(pm25, 0)
44
  breakpoints = [
45
- (0.0, 12.0, 0, 50),
46
- (12.1, 35.4, 51, 100),
47
- (35.5, 55.4, 101, 150),
48
- (55.5, 150.4, 151, 200),
49
- (150.5, 250.4, 201, 300),
50
- (250.5, 350.4, 301, 400),
51
- (350.5, 500.4, 401, 500),
52
  ]
53
  for lo, hi, aqi_lo, aqi_hi in breakpoints:
54
  if pm25 <= hi:
@@ -56,196 +55,105 @@ def pm25_to_aqi(pm25: float) -> float:
56
  return 500.0
57
 
58
  AQI_BANDS = [
59
- (0, 50, "Good", "#2ecc71", "#d4efdf"),
60
- (51, 100, "Moderate", "#f1c40f", "#fef9e7"),
61
- (101, 150, "Unhealthy for Sensitive Groups", "#e67e22", "#fdebd0"),
62
- (151, 200, "Unhealthy", "#e74c3c", "#fadbd8"),
63
- (201, 300, "Very Unhealthy", "#8e44ad", "#e8daef"),
64
- (301, 999, "Hazardous", "#922b21", "#f5b7b1"),
65
  ]
66
 
67
  AQI_LEGEND_HTML = """
68
  <div class="aqi-legend">
69
  <h3>AQI Categories</h3>
70
  <table>
71
- <thead>
72
- <tr>
73
- <th>AQI Range</th>
74
- <th>Category</th>
75
- <th>Status</th>
76
- </tr>
77
- </thead>
78
  <tbody>
79
  """
80
- for lo, hi, category, color, bgcolor in AQI_BANDS:
81
- AQI_LEGEND_HTML += f"""
82
- <tr style="background-color: {bgcolor};">
83
- <td>{lo} - {hi}</td>
84
- <td>{category}</td>
85
- <td style="background-color: {color}; width: 20px;"></td>
86
- </tr>
87
- """
88
  AQI_LEGEND_HTML += "</tbody></table></div>"
89
 
90
- KARACHI_STATIONS = [
91
- {"name": "US Consulate", "lat": 24.8494, "lon": 67.0111},
92
- {"name": "Korangi", "lat": 24.8286, "lon": 67.1147},
93
- {"name": "Gulshan-e-Iqbal", "lat": 24.9161, "lon": 67.0911},
94
- ]
95
-
96
- def aqi_meta(value):
97
- for lo, hi, cat, color, bg in AQI_BANDS:
98
- if value <= hi: return cat, color, bg
99
- return "Hazardous", "#922b21", "#f5b7b1"
100
-
101
- # --- Data Fetching ---
102
  API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
103
  KARACHI_LAT, KARACHI_LON = 24.8607, 67.0011
104
 
105
- def fetch_historical_pm25(lat: float, lon: float, hours: int = 24) -> List[float]:
106
  if not API_KEY:
107
- return [35.0 + np.random.normal(0, 5) for _ in range(hours)] # Sample data fallback
108
-
109
- end = datetime.now(timezone.utc)
110
- start = end - timedelta(hours=hours)
111
- url = "http://api.openweathermap.org/data/2.5/air_pollution/history"
112
- params = {"lat": lat, "lon": lon, "start": int(start.timestamp()), "end": int(end.timestamp()), "appid": API_KEY}
113
-
114
- resp = requests.get(url, params=params)
115
- if resp.status_code != 200:
116
- return [35.0 + np.random.normal(0, 5) for _ in range(hours)]
117
-
118
- data = resp.json().get("list", [])
119
- pm25_values = [entry.get("components", {}).get("pm2_5", 0.0) for entry in data]
120
- return pm25_values[-hours:] if len(pm25_values) >= hours else pm25_values
121
-
122
- def forecast_from_array(past_values: np.ndarray, horizon: int = 24) -> np.ndarray:
123
- # Prepare input for TimesFM
124
- inputs = torch.from_numpy(past_values).float().unsqueeze(0).unsqueeze(0)
125
  with torch.no_grad():
126
- outputs = model(inputs, prediction_length=horizon)
127
- # Extract mean forecast
128
- forecast = outputs.point_forecast.squeeze().numpy()
129
- return forecast
130
-
131
- def get_real_forecast_data(hours: int = 24):
132
- pm25_history = fetch_historical_pm25(KARACHI_LAT, KARACHI_LON, hours=48)
133
- aqi_history = [pm25_to_aqi(v) for v in pm25_history]
134
- history_arr = np.array(aqi_history[-24:])
135
- forecast_arr = forecast_from_array(history_arr, hours)
136
- return history_arr, forecast_arr, hours
137
-
138
- # --- Visualizations ---
139
- def make_forecast_plot(history, forecast, horizon):
140
- fig = go.Figure()
141
- fig.add_trace(go.Scatter(y=history, name="History", line=dict(color="blue")))
142
- fig.add_trace(go.Scatter(x=list(range(len(history), len(history)+len(forecast))), y=forecast, name="Forecast", line=dict(color="red", dash="dash")))
143
- fig.update_layout(title="AQI Forecast", template="plotly_white", height=400)
144
- return fig
145
 
146
- def make_hourly_heatmap(forecast, horizon):
147
- fig = go.Figure(data=go.Heatmap(z=[forecast], colorscale="Viridis"))
148
- fig.update_layout(title="Hourly Intensity", height=200)
149
- return fig
 
 
 
 
 
 
 
 
150
 
151
- def make_distribution_plot(history, forecast):
 
152
  fig = go.Figure()
153
- fig.add_trace(go.Histogram(x=history, name="History", opacity=0.5))
154
- fig.add_trace(go.Histogram(x=forecast, name="Forecast", opacity=0.5))
155
- fig.update_layout(barmode='overlay', title="AQI Distribution")
156
  return fig
157
 
158
- def make_rate_of_change_plot(forecast):
159
- roc = np.diff(forecast)
160
- fig = go.Figure(go.Scatter(y=roc, mode='lines+markers', name="Rate of Change"))
161
- fig.update_layout(title="Hourly AQI Change")
162
- return fig
163
-
164
- def make_station_map(current_forecast_mean: float = None):
165
- # Mock map for demonstration
166
- fig = go.Figure(go.Scattermapbox(lat=[s['lat'] for s in KARACHI_STATIONS], lon=[s['lon'] for s in KARACHI_STATIONS], mode='markers+text', marker=dict(size=15), text=[s['name'] for s in KARACHI_STATIONS]))
167
- fig.update_layout(mapbox_style="carto-positron", mapbox_zoom=10, mapbox_center={"lat": KARACHI_LAT, "lon": KARACHI_LON})
168
- return fig
169
-
170
- def build_kpi_html(forecast, history):
171
- current = forecast[0]
172
- cat, color, bg = aqi_meta(current)
173
- return f"<div style='background:{bg}; padding:20px; border-radius:10px; text-align:center;'><h2 style='color:{color};'>Current AQI: {current:.1f}</h2><h3>Category: {cat}</h3></div>"
174
-
175
- # --- AI Advisor ---
176
- def chat_with_advisor(user_message, chat_history, history_arr, forecast_arr, horizon):
177
- if not os.getenv("ANTHROPIC_API_KEY"):
178
- return chat_history + [[user_message, "Please set ANTHROPIC_API_KEY to use the advisor."]], ""
179
-
180
- context = f"History: {history_arr.tolist()}\nForecast: {forecast_arr.tolist()}"
181
- response = anthropic_client.messages.create(
182
- model="claude-3-5-sonnet-20240620",
183
- max_tokens=500,
184
- system="You are an AQI health advisor for Karachi. Provide actionable advice based on the forecast.",
185
- messages=[{"role": "user", "content": f"Context: {context}\nQuestion: {user_message}"}]
186
- )
187
- answer = response.content[0].text
188
- chat_history.append((user_message, answer))
189
- return chat_history, ""
190
-
191
  # --- Gradio UI ---
192
- CSS = """
193
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
194
- .aqi-legend table { width: 100%; border-collapse: collapse; margin-top: 10px; }
195
- .aqi-legend th, .aqi-legend td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size: 0.8rem; }
196
- .aqi-legend th { background-color: #f2f2f2; }
197
- .sky-chat { border-radius: 10px; }
198
- """
199
 
200
- def refresh_dashboard(horizon):
201
- try:
202
- hist, fc, hor = get_real_forecast_data(horizon)
203
- kpi = build_kpi_html(fc, hist)
204
- p1 = make_forecast_plot(hist, fc, hor)
205
- p2 = make_hourly_heatmap(fc, hor)
206
- p3 = make_distribution_plot(hist, fc)
207
- p4 = make_rate_of_change_plot(fc)
208
- p5 = make_station_map(float(np.mean(fc)))
209
- return kpi, p1, p2, p3, p4, p5, fc.tolist(), hist.tolist()
210
- except Exception as e:
211
- print(f"Error: {e}")
212
- return "Error loading data", None, None, None, None, None, [], []
213
-
214
- with gr.Blocks(title="Skyloom – Live Karachi AQI Forecaster", css=CSS) as demo:
215
- forecast_state = gr.State([])
216
- history_state = gr.State([])
217
-
218
- gr.HTML("<div style='text-align:center;'><h1>🌬️ Skyloom – Karachi AQI</h1><p>AI-Powered Forecasts</p></div>")
219
 
 
 
 
 
220
  with gr.Row():
221
  with gr.Column(scale=1):
222
- horizon = gr.Slider(label="Forecast Horizon (Hours)", minimum=1, maximum=MAX_HORIZON, value=24)
223
- refresh_btn = gr.Button("🔄 Refresh Data", variant="primary")
224
- with gr.Accordion("📘 AQI Reference", open=False):
225
  gr.HTML(AQI_LEGEND_HTML)
226
 
227
  with gr.Column(scale=2):
228
- kpi_html = gr.HTML()
229
- with gr.Tabs():
230
- with gr.Tab("📈 Forecast"): plot_main = gr.Plot()
231
- with gr.Tab("🌡️ Heatmap"): plot_heat = gr.Plot()
232
- with gr.Tab("📊 Distribution"): plot_dist = gr.Plot()
233
- with gr.Tab("🗺️ Map"): plot_map = gr.Plot()
234
- with gr.Tab("🤖 AI Advisor"):
235
- chatbot = gr.Chatbot(label="Health Advisor", height=300)
236
- chat_input = gr.Textbox(placeholder="Ask about the forecast...")
237
- send_btn = gr.Button("Send")
238
-
239
- ALL_OUTPUTS = [kpi_html, plot_main, plot_heat, plot_dist, plot_roc if 'plot_roc' in locals() else plot_dist, plot_map, forecast_state, history_state]
240
-
241
- # Fix ALL_OUTPUTS to match the refresh function
242
- ALL_OUTPUTS = [kpi_html, plot_main, plot_heat, plot_dist, gr.State(), plot_map, forecast_state, history_state]
243
-
244
- def on_refresh(hor):
245
- return refresh_dashboard(hor)
246
-
247
- refresh_btn.click(fn=on_refresh, inputs=horizon, outputs=[kpi_html, plot_main, plot_heat, plot_dist, gr.State(), plot_map, forecast_state, history_state])
248
- send_btn.click(chat_with_advisor, inputs=[chat_input, chatbot, history_state, forecast_state, horizon], outputs=[chatbot, chat_input])
249
 
250
  if __name__ == "__main__":
251
  demo.launch()
 
6
  import requests
7
  import os
8
  from datetime import datetime, timezone, timedelta
9
+ from transformers import TimesFm2_5ModelForPrediction, AutoModelForCausalLM, AutoTokenizer, pipeline
10
  from typing import List, Optional
11
 
12
  # --- Configuration & Model Loading ---
13
+ TIMESFM_MODEL_ID = "mahwizzzz/skyloom"
14
+ # Using a very lightweight open-source model for the advisor to fit in CPU RAM
15
+ ADVISOR_MODEL_ID = "HuggingFaceTB/SmolLM2-135M-Instruct"
16
 
17
+ print("Loading Models …")
18
+ device = "cpu" # Force CPU for Hugging Face Space stability
 
19
 
20
+ # 1. Load TimesFM (Forecaster)
21
+ forecaster_model = TimesFm2_5ModelForPrediction.from_pretrained(
22
+ TIMESFM_MODEL_ID,
23
  torch_dtype=torch.float32,
24
+ )
25
+ print("Quantizing Forecaster...")
26
+ forecaster_model = torch.quantization.quantize_dynamic(
27
+ forecaster_model, {torch.nn.Linear}, dtype=torch.qint8
28
  )
29
 
30
+ # 2. Load SmolLM2 (Advisor - Open Source & Tiny)
31
+ print("Loading Advisor (SmolLM2)...")
32
+ advisor_tokenizer = AutoTokenizer.from_pretrained(ADVISOR_MODEL_ID)
33
+ advisor_model = AutoModelForCausalLM.from_pretrained(
34
+ ADVISOR_MODEL_ID,
35
+ torch_dtype=torch.float32,
36
+ )
37
+ # Quantize the advisor model too for speed
38
+ advisor_model = torch.quantization.quantize_dynamic(
39
+ advisor_model, {torch.nn.Linear}, dtype=torch.qint8
40
+ )
41
 
42
+ print("Models ready on CPU")
43
 
44
  # --- AQI Logic ---
45
  def pm25_to_aqi(pm25: float) -> float:
46
  pm25 = max(pm25, 0)
47
  breakpoints = [
48
+ (0.0, 12.0, 0, 50), (12.1, 35.4, 51, 100), (35.5, 55.4, 101, 150),
49
+ (55.5, 150.4, 151, 200), (150.5, 250.4, 201, 300), (250.5, 350.4, 301, 400),
50
+ (350.5, 500.4, 401, 500)
 
 
 
 
51
  ]
52
  for lo, hi, aqi_lo, aqi_hi in breakpoints:
53
  if pm25 <= hi:
 
55
  return 500.0
56
 
57
  AQI_BANDS = [
58
+ (0, 50, "Good", "#2ecc71", "#d4efdf"),
59
+ (51, 100, "Moderate", "#f1c40f", "#fef9e7"),
60
+ (101, 150, "Unhealthy for Sensitive Groups", "#e67e22", "#fdebd0"),
61
+ (151, 200, "Unhealthy", "#e74c3c", "#fadbd8"),
62
+ (201, 300, "Very Unhealthy", "#8e44ad", "#e8daef"),
63
+ (301, 999, "Hazardous", "#922b21", "#f5b7b1"),
64
  ]
65
 
66
  AQI_LEGEND_HTML = """
67
  <div class="aqi-legend">
68
  <h3>AQI Categories</h3>
69
  <table>
70
+ <thead><tr><th>AQI</th><th>Category</th><th>Color</th></tr></thead>
 
 
 
 
 
 
71
  <tbody>
72
  """
73
+ for lo, hi, cat, color, bg in AQI_BANDS:
74
+ AQI_LEGEND_HTML += f"<tr style='background-color:{bg};'><td>{lo}-{hi}</td><td>{cat}</td><td style='background-color:{color};'></td></tr>"
 
 
 
 
 
 
75
  AQI_LEGEND_HTML += "</tbody></table></div>"
76
 
77
+ # --- Data Fetching (Karachi) ---
 
 
 
 
 
 
 
 
 
 
 
78
  API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
79
  KARACHI_LAT, KARACHI_LON = 24.8607, 67.0011
80
 
81
+ def fetch_data(hours=24):
82
  if not API_KEY:
83
+ return [30 + np.random.normal(0, 5) for _ in range(hours)]
84
+ url = f"http://api.openweathermap.org/data/2.5/air_pollution/history?lat={KARACHI_LAT}&lon={KARACHI_LON}&start={int((datetime.now()-timedelta(hours=hours)).timestamp())}&end={int(datetime.now().timestamp())}&appid={API_KEY}"
85
+ try:
86
+ r = requests.get(url).json()
87
+ return [e['components']['pm2_5'] for e in r['list']][-hours:]
88
+ except:
89
+ return [30 + np.random.normal(0, 5) for _ in range(hours)]
90
+
91
+ # --- Inference ---
92
+ def get_forecast(history, horizon=24):
93
+ inputs = torch.tensor(history).float().view(1, 1, -1)
 
 
 
 
 
 
 
94
  with torch.no_grad():
95
+ out = forecaster_model(inputs, prediction_length=horizon)
96
+ return out.point_forecast.numpy().flatten()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # --- Advisor Logic (Open Source) ---
99
+ def ask_advisor(msg, history, fc):
100
+ current_aqi = fc[0] if len(fc) > 0 else 0
101
+ prompt = f"<|im_start|>system\nYou are an AQI health advisor for Karachi. Current AQI is {current_aqi:.1f}. Give short, helpful health advice.<|im_end|>\n<|im_start|>user\n{msg}<|im_end|>\n<|im_start|>assistant\n"
102
+
103
+ inputs = advisor_tokenizer(prompt, return_tensors="pt")
104
+ with torch.no_grad():
105
+ outputs = advisor_model.generate(**inputs, max_new_tokens=100, do_sample=True, temperature=0.7)
106
+
107
+ answer = advisor_tokenizer.decode(outputs[0], skip_special_tokens=True).split("assistant\n")[-1]
108
+ history.append((msg, answer))
109
+ return history, ""
110
 
111
+ # --- Visuals ---
112
+ def make_plot(hist, fc):
113
  fig = go.Figure()
114
+ fig.add_trace(go.Scatter(y=hist, name="History", line=dict(color="blue")))
115
+ fig.add_trace(go.Scatter(x=list(range(len(hist), len(hist)+len(fc))), y=fc, name="Forecast", line=dict(color="red", dash="dash")))
116
+ fig.update_layout(title="Karachi AQI Forecast", template="plotly_white", margin=dict(l=20, r=20, t=40, b=20))
117
  return fig
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  # --- Gradio UI ---
120
+ CSS = ".aqi-legend table { width: 100%; border-collapse: collapse; } .aqi-legend td { padding: 5px; border: 1px solid #eee; }"
 
 
 
 
 
 
121
 
122
+ def update_ui(horizon):
123
+ pm_hist = fetch_data(24)
124
+ aqi_hist = [pm25_to_aqi(p) for p in pm_hist]
125
+ fc = get_forecast(aqi_hist, horizon)
126
+
127
+ current_aqi = fc[0]
128
+ for lo, hi, cat, color, bg in AQI_BANDS:
129
+ if current_aqi <= hi:
130
+ status_html = f"<div style='background:{bg}; color:{color}; padding:15px; border-radius:10px; text-align:center;'><h2>Current: {current_aqi:.1f} ({cat})</h2></div>"
131
+ break
132
+
133
+ return status_html, make_plot(aqi_hist, fc), fc.tolist()
 
 
 
 
 
 
 
134
 
135
+ with gr.Blocks(css=CSS, title="Skyloom Karachi") as demo:
136
+ fc_state = gr.State([])
137
+ gr.Markdown("# 🌬️ Skyloom Karachi\n*CPU-Optimized Open-Source AQI Forecaster*")
138
+
139
  with gr.Row():
140
  with gr.Column(scale=1):
141
+ horizon = gr.Slider(label="Forecast Hours", minimum=1, maximum=72, value=24)
142
+ btn = gr.Button("🔄 Predict Current", variant="primary")
143
+ with gr.Accordion("📘 AQI Legend", open=False):
144
  gr.HTML(AQI_LEGEND_HTML)
145
 
146
  with gr.Column(scale=2):
147
+ status = gr.HTML("Click Predict to start...")
148
+ plot = gr.Plot()
149
+
150
+ with gr.Tab("🤖 Open-Source Advisor"):
151
+ chatbot = gr.Chatbot(height=300)
152
+ msg_input = gr.Textbox(placeholder="Ask about health precautions...")
153
+ send = gr.Button("Ask Advisor")
154
+
155
+ btn.click(update_ui, inputs=horizon, outputs=[status, plot, fc_state])
156
+ send.click(ask_advisor, inputs=[msg_input, chatbot, fc_state], outputs=[chatbot, msg_input])
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  if __name__ == "__main__":
159
  demo.launch()