skyloom / app.py
mahwizzzz's picture
Create app.py
ba8c095 verified
Raw
History Blame
6.34 kB
import gradio as gr
import torch
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from transformers import TimesFm2_5ModelForPrediction
from datetime import datetime, timedelta
import tempfile
import os
MODEL_ID = "mahwizzzz/skyloom"
CONTEXT_LEN = 1024
MAX_HORIZON = 72
print("Loading model from Hub...")
device = "cuda" if torch.cuda.is_available() else "cpu"
model = TimesFm2_5ModelForPrediction.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32,
device_map=device if device == "cuda" else None
)
model.eval()
print("Model ready")
def forecast_from_array(past_values: np.ndarray, horizon: int = 24) -> np.ndarray:
if len(past_values) < CONTEXT_LEN:
pad_len = CONTEXT_LEN - len(past_values)
past_values = np.pad(past_values, (pad_len, 0), mode='edge')
else:
past_values = past_values[-CONTEXT_LEN:]
input_tensor = torch.tensor(past_values[None, :], dtype=torch.float32).to(device)
with torch.no_grad():
output = model(past_values=input_tensor, forecast_context_len=CONTEXT_LEN)
forecast = output.mean_predictions[0, :horizon].float().cpu().numpy()
return np.clip(forecast, 0, None)
def load_csv(file):
df = pd.read_csv(file.name)
if 'aqi' in df.columns:
values = df['aqi'].values
else:
values = df.iloc[:, 0].values
return values.astype(float)
def forecast_from_csv(file, horizon):
values = load_csv(file)
if len(values) < 2:
raise gr.Error("CSV must contain at least 2 AQI values.")
forecast = forecast_from_array(values, horizon)
return create_plot(values[-CONTEXT_LEN:], forecast, horizon)
def forecast_from_text(text, horizon):
try:
values = np.array([float(x.strip()) for x in text.split(',') if x.strip()])
except:
raise gr.Error("Please enter a comma-separated list of numbers (e.g., 45.2, 47.1, 46.8)")
if len(values) < 2:
raise gr.Error("At least 2 values required.")
forecast = forecast_from_array(values, horizon)
return create_plot(values[-CONTEXT_LEN:], forecast, horizon)
def create_plot(history, forecast, horizon):
hist_indices = list(range(-len(history), 0))
fore_indices = list(range(1, horizon+1))
fig = go.Figure()
fig.add_trace(go.Scatter(
x=hist_indices, y=history,
mode='lines+markers',
name='Past AQI',
line=dict(color='#1f77b4', width=2),
marker=dict(size=4)
))
fig.add_trace(go.Scatter(
x=fore_indices, y=forecast,
mode='lines+markers',
name='Forecast',
line=dict(color='#ff7f0e', width=3, dash='dot'),
marker=dict(size=6, symbol='triangle-up')
))
fig.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.5)
fig.add_annotation(x=0, y=max(history[-10:]+forecast[:5]), text="Forecast start", showarrow=False, yshift=10)
fig.update_layout(
title=f"AQI Forecast – Next {horizon} hours",
xaxis_title="Hours relative to present (0 = current hour)",
yaxis_title="Air Quality Index (AQI)",
template="plotly_white",
hovermode="x unified",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
return fig
with gr.Blocks(theme=gr.themes.Soft(), title="Skyloom – Karachi AQI Forecaster", css="footer {visibility: hidden}") as demo:
gr.Markdown("""
# Skyloom: AQI Forecaster for Karachi
**TimesFM‑2.5** – Fine‑tuned for hourly Air Quality Index predictions.
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Input Data")
input_method = gr.Radio(["Paste values", "Upload CSV"], label="Input method", value="Paste values")
with gr.Group(visible=True) as text_group:
text_input = gr.Textbox(
label="Past AQI values (comma‑separated, at least 24 values, oldest first)",
placeholder="45.2, 47.1, 46.8, 48.3, ...",
lines=3
)
with gr.Group(visible=False) as csv_group:
csv_input = gr.File(label="Upload CSV", file_types=[".csv"])
gr.Markdown("CSV should have a column named `aqi` or the first column containing hourly AQI values.")
horizon_slider = gr.Slider(label="Forecast horizon (hours)", minimum=1, maximum=72, value=24, step=1)
forecast_btn = gr.Button("Generate Forecast", variant="primary")
with gr.Column(scale=2):
plot_output = gr.Plot(label="Forecast Chart")
gr.Markdown("""
**Interpretation**
- **Past AQI** (blue): last 1024 hours of history (or the portion you provided)
- **Forecast** (orange dashed): predicted AQI for the next hours
- AQI categories:
- 0–50 Good
- 51–100 Moderate
- 101–150 Unhealthy for Sensitive Groups
- 151–200 Unhealthy
- 201+ Very Unhealthy / Hazardous
""")
def toggle_visibility(method):
return gr.update(visible=(method == "Paste values")), gr.update(visible=(method == "Upload CSV"))
input_method.change(toggle_visibility, inputs=input_method, outputs=[text_group, csv_group])
def on_text_forecast(text, horizon):
if not text.strip():
raise gr.Error("Please enter comma-separated AQI values.")
return forecast_from_text(text, horizon)
def on_csv_forecast(file, horizon):
if file is None:
raise gr.Error("Please upload a CSV file.")
return forecast_from_csv(file, horizon)
forecast_btn.click(
fn=lambda method, text, csv, horizon:
forecast_from_text(text, horizon) if method == "Paste values" else forecast_from_csv(csv, horizon),
inputs=[input_method, text_input, csv_input, horizon_slider],
outputs=plot_output
)
gr.Markdown("""
---
**Model info**
- Base model: `google/timesfm-2.5-200m-transformers`
- Test MAE: **5.18** | sMAPE: **5.46%** | MASE: **0.47**
- *Disclaimer*: Forecasts are for informational purposes only. Sudden pollution events may not be captured.
""")
if __name__ == "__main__":
demo.launch()