skyloom / app.py
mahwizzzz's picture
Update app.py
d2d79eb verified
Raw
History Blame Contribute Delete
11.7 kB
import gradio as gr
import torch
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import requests
import os
from datetime import datetime, timezone, timedelta
from transformers import TimesFm2_5ModelForPrediction, AutoModelForCausalLM, AutoTokenizer
from typing import List, Optional
from ui_components import CSS, get_header_html, make_metric_cards, make_day_cards, get_status_hero, AQI_BANDS, AQI_TIPS
from viz_components import make_gauge, make_plot, make_map
TIMESFM_MODEL_ID = "mahwizzzz/skyloom"
ADVISOR_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
print("Loading Models …")
device = "cpu"
forecaster_model = TimesFm2_5ModelForPrediction.from_pretrained(
TIMESFM_MODEL_ID,
dtype=torch.float32,
)
forecaster_model.eval()
print("Loading Advisor (Qwen2.5-0.5B)...")
advisor_tokenizer = AutoTokenizer.from_pretrained(ADVISOR_MODEL_ID)
advisor_model = AutoModelForCausalLM.from_pretrained(
ADVISOR_MODEL_ID,
dtype=torch.float32,
)
advisor_model.eval()
print("Models ready on CPU")
def pm25_to_aqi(pm25: float) -> float:
pm25 = max(pm25, 0)
breakpoints = [
(0.0, 12.0, 0, 50), (12.1, 35.4, 51, 100), (35.5, 55.4, 101, 150),
(55.5, 150.4, 151, 200), (150.5, 250.4, 201, 300), (250.5, 350.4, 301, 400),
(350.5, 500.4, 401, 500)
]
for lo, hi, aqi_lo, aqi_hi in breakpoints:
if pm25 <= hi:
return ((aqi_hi - aqi_lo) / (hi - lo)) * (pm25 - lo) + aqi_lo
return 500.0
API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
KARACHI_LAT, KARACHI_LON = 24.8607, 67.0011
def fetch_data(hours=24):
if not API_KEY:
return [30 + np.random.normal(0, 5) for _ in range(hours)]
url = (
f"http://api.openweathermap.org/data/2.5/air_pollution/history"
f"?lat={KARACHI_LAT}&lon={KARACHI_LON}"
f"&start={int((datetime.now()-timedelta(hours=hours)).timestamp())}"
f"&end={int(datetime.now().timestamp())}&appid={API_KEY}"
)
try:
r = requests.get(url).json()
return [e['components']['pm2_5'] for e in r['list']][-hours:]
except:
return [30 + np.random.normal(0, 5) for _ in range(hours)]
def get_forecast(history, horizon=24):
inputs = [torch.tensor(history, dtype=torch.float32)]
with torch.no_grad():
out = forecaster_model(past_values=inputs, prediction_length=horizon, return_dict=True)
return out.mean_predictions[0].numpy().flatten()
def ask_advisor(msg, history, fc):
current_aqi = fc[0] if len(fc) > 0 else 0
category = "Good"
for lo, hi, cat, color, bg in AQI_BANDS:
if current_aqi <= hi:
category = cat
break
tip = AQI_TIPS.get(category, "")
system_prompt = (
f"You are Skyloom, an air-quality assistant for Karachi, Pakistan. "
f"Current AQI in Karachi: {current_aqi:.0f}, category: {category}. "
f"Recommendation for this AQI level: {tip} "
f"Answer the user's question directly based ONLY on this AQI information. "
f"Keep your answer to 1-2 sentences. Do not discuss unrelated topics."
)
chat = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": msg},
]
prompt = advisor_tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
inputs = advisor_tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
outputs = advisor_model.generate(
**inputs,
max_new_tokens=80,
do_sample=False,
repetition_penalty=1.15,
no_repeat_ngram_size=3,
pad_token_id=advisor_tokenizer.eos_token_id,
)
new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
answer = advisor_tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
if not answer:
answer = tip
history = history + [{"role": "user", "content": msg}, {"role": "assistant", "content": answer}]
return history, ""
def update_ui(horizon):
try:
pm_hist = fetch_data(24)
aqi_hist = [pm25_to_aqi(p) for p in pm_hist]
fc = get_forecast(aqi_hist, horizon)
current_aqi = fc[0]
status_hero = get_status_hero(current_aqi)
gauge_fig = make_gauge(current_aqi)
metric_html = make_metric_cards(pm_hist[-1], fc)
day_html = make_day_cards(fc)
trend_plot = make_plot(aqi_hist, fc, window="24h")
map_fig = make_map(current_aqi)
share_html = make_share_card(current_aqi)
return status_hero, gauge_fig, metric_html, day_html, trend_plot, map_fig, share_html, fc.tolist()
except Exception as e:
print(f"Error: {e}")
empty = go.Figure()
return "<div>Error loading data</div>", empty, "", "", empty, empty, "", []
def update_trend(window, fc_state):
"""Re-render the trend chart when user changes the window toggle."""
try:
pm_hist = fetch_data(24)
aqi_hist = [pm25_to_aqi(p) for p in pm_hist]
fc = np.array(fc_state) if fc_state else np.array(aqi_hist)
return make_plot(aqi_hist, fc, window=window)
except Exception as e:
print(f"Trend error: {e}")
return go.Figure()
def make_share_card(aqi):
category = "Good"
color = "#10b981"
tip = ""
for lo, hi, cat, col, bg in AQI_BANDS:
if aqi <= hi:
category, color = cat, col
tip = AQI_TIPS.get(cat, "")
break
now = datetime.now().strftime("%d %b %Y, %H:%M")
return f"""
<div id="share-card" style="
background: linear-gradient(135deg, {color}22, {color}08);
border: 2px solid {color}44;
border-radius: 20px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 12px;
">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<div style="font-size:0.8rem; color:#64748b; font-weight:600; text-transform:uppercase; letter-spacing:0.05em;">
Karachi AQI Snapshot
</div>
<div style="font-size:0.75rem; color:#94a3b8;">{now}</div>
</div>
<div style="font-size:1.4rem;">🌬️</div>
</div>
<div style="display:flex; align-items:baseline; gap:10px;">
<span style="font-size:3rem; font-weight:800; color:{color}; line-height:1;">{aqi:.0f}</span>
<span style="font-size:1rem; color:#64748b;">AQI</span>
</div>
<div style="
display:inline-block;
background:{color};
color:white;
font-size:0.8rem;
font-weight:700;
padding:4px 14px;
border-radius:999px;
width:fit-content;
">{category}</div>
<div style="font-size:0.85rem; color:#475569; line-height:1.5;">{tip}</div>
<button onclick="
const card = document.getElementById('share-card');
const text = 'Karachi AQI: {aqi:.0f} ({category}) as of {now}. {tip} — via Skyloom';
if (navigator.share) {{
navigator.share({{ title: 'Skyloom AQI', text: text }});
}} else {{
navigator.clipboard.writeText(text).then(() => alert('Copied to clipboard!'));
}}
" style="
background: {color};
color: white;
border: none;
border-radius: 12px;
padding: 10px 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-top: 4px;
">📤 Share AQI Snapshot</button>
</div>
"""
with gr.Blocks(title="Karachi Air Quality") as demo:
fc_state = gr.State([])
with gr.Column(elem_id="main-container"):
gr.HTML(get_header_html())
with gr.Tabs():
with gr.Tab("🏠 Dashboard"):
status_hero = gr.HTML(
"<div class='status-hero' style='background:#94a3b8'>"
"<p>Current Observation</p><h1>--</h1><p>Loading…</p></div>"
)
with gr.Row():
# Left column
with gr.Column(scale=1):
with gr.Column(elem_classes="mobile-card"):
gr.Markdown("### Air Quality Index")
gauge = gr.Plot(show_label=False)
horizon = gr.Slider(
label="Forecast Horizon (hours)",
minimum=1, maximum=72, value=24, step=1
)
btn = gr.Button("🔄 Update Forecast", elem_id="predict-btn")
with gr.Column(scale=1):
with gr.Column(elem_classes="mobile-card"):
gr.Markdown("### Current Observation")
metrics = gr.HTML()
with gr.Column(elem_classes="mobile-card"):
gr.Markdown("### Weekly Outlook")
day_cards = gr.HTML()
# Trend chart with window toggle
with gr.Column(elem_classes="mobile-card"):
with gr.Row():
gr.Markdown("### AQI Trend")
window_radio = gr.Radio(
choices=["24h", "7d", "30d"],
value="24h",
label="",
show_label=False,
elem_id="window-radio",
)
plot = gr.Plot(show_label=False)
with gr.Row():
with gr.Column(scale=2, elem_classes="mobile-card"):
gr.Markdown("### 📍 Monitoring Station Karachi")
map_plot = gr.Plot(show_label=False)
with gr.Column(scale=1, elem_classes="mobile-card"):
gr.Markdown("### 📤 Share Snapshot")
share_card = gr.HTML()
with gr.Tab("🤖 AI Advisor"):
with gr.Column(elem_classes="mobile-card"):
gr.Markdown("### Skyloom Health Advisor")
chatbot = gr.Chatbot(height=420)
with gr.Row():
msg_input = gr.Textbox(
placeholder="Ask about health precautions…",
scale=4, show_label=False
)
send = gr.Button("Ask", variant="primary", scale=1)
gr.Examples(
examples=[
"Is it safe to exercise outdoors?",
"Should I wear a mask?",
"Can I open my windows?",
"Is it safe for children to play outside?",
],
inputs=msg_input,
)
outputs_full = [status_hero, gauge, metrics, day_cards, plot, map_plot, share_card, fc_state]
btn.click(update_ui, inputs=horizon, outputs=outputs_full)
demo.load(update_ui, inputs=horizon, outputs=outputs_full)
window_radio.change(update_trend, inputs=[window_radio, fc_state], outputs=plot)
send.click(ask_advisor, inputs=[msg_input, chatbot, fc_state], outputs=[chatbot, msg_input])
msg_input.submit(ask_advisor, inputs=[msg_input, chatbot, fc_state], outputs=[chatbot, msg_input])
if __name__ == "__main__":
demo.queue(max_size=5)
demo.launch(css=CSS)