import gradio as gr import numpy as np import pandas as pd import torch import pickle from datetime import datetime, timedelta, timezone import os import requests import sys from huggingface_hub import hf_hub_download from importlib.util import spec_from_file_location, module_from_spec # ============================================================================ # CONFIGURATION # ============================================================================ MODEL_REPO_ID = "assix-research/gcc-weather-forecast-transformer" DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"š Using device: {DEVICE}") # ============================================================================ # š„ DOWNLOAD ARTIFACTS # ============================================================================ print("\nš¦ DOWNLOADING MODEL ARTIFACTS...") def load_artifact(filename): try: path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=filename) return path except Exception as e: print(f"ā Failed to download {filename}: {e}") return None scaler_path = load_artifact("feature_scaler.pkl") metadata_path = load_artifact("metadata.pkl") model_weights_path = load_artifact("weather_transformer.pt") model_code_path = load_artifact("model.py") # ============================================================================ # LOAD RESOURCES # ============================================================================ FEATURES = ['Temperature', 'Humidity', 'Pressure', 'Wind'] # Fallback SEQUENCE_LENGTH = 72 if scaler_path: with open(scaler_path, 'rb') as f: feature_scaler = pickle.load(f) if hasattr(feature_scaler, 'feature_names_in_'): FEATURES = list(feature_scaler.feature_names_in_) print(f"ā TRUSTING SCALER. True Training Order: {FEATURES}") else: print("ā ļø Scaler has no internal names, falling back to metadata.") if metadata_path: with open(metadata_path, 'rb') as f: metadata = pickle.load(f) SEQUENCE_LENGTH = metadata.get('sequence_length', 72) model = None if model_code_path and model_weights_path: try: spec = spec_from_file_location("weather_model", model_code_path) weather_model = module_from_spec(spec) spec.loader.exec_module(weather_model) model = weather_model.WeatherTransformer( input_dim=len(FEATURES), d_model=256, n_heads=8, n_layers=4, d_ff=512, dropout=0.1, seq_len=SEQUENCE_LENGTH ).to(DEVICE) state_dict = torch.load(model_weights_path, map_location=DEVICE) model.load_state_dict(state_dict) model.eval() print("ā Model successfully loaded!") except Exception as e: print(f"ā Error initializing model: {e}") # ============================================================================ # DATA HELPERS # ============================================================================ def format_local_datetime(dt_utc): if hasattr(dt_utc, 'to_pydatetime'): dt_utc = dt_utc.to_pydatetime() if dt_utc.tzinfo is None: dt_utc = dt_utc.replace(tzinfo=timezone.utc) dt_local = dt_utc.astimezone() return dt_local.strftime("%H:%M") def fetch_weather_context(city_name): locations = { 'Dubai, UAE': {'lat': 25.2048, 'lon': 55.2708}, 'Riyadh, Saudi Arabia': {'lat': 24.7136, 'lon': 46.6753} } if city_name not in locations: return None coords = locations[city_name] end_date = datetime.now(timezone.utc) start_date = end_date - timedelta(hours=100) url = "https://archive-api.open-meteo.com/v1/archive" params = { 'latitude': coords['lat'], 'longitude': coords['lon'], 'start_date': start_date.strftime("%Y-%m-%d"), 'end_date': end_date.strftime("%Y-%m-%d"), 'hourly': 'temperature_2m,relative_humidity_2m,pressure_msl,wind_speed_10m', 'timezone': 'UTC' } try: r = requests.get(url, params=params) r.raise_for_status() data = r.json() df = pd.DataFrame({ 'time': pd.to_datetime(data['hourly']['time']), 'temperature_2m': data['hourly']['temperature_2m'], 'relative_humidity_2m': data['hourly']['relative_humidity_2m'], 'pressure_msl': data['hourly']['pressure_msl'], 'wind_speed_10m': data['hourly']['wind_speed_10m'] }) if len(df) >= SEQUENCE_LENGTH: return df.tail(SEQUENCE_LENGTH) return None except Exception: return None # ============================================================================ # CORE PREDICTION LOGIC # ============================================================================ def predict_weather(city, hours_ahead): if model is None or feature_scaler is None: return "System initializing...", None, None, gr.update(visible=True), "Temperature" # 1. Fetch History df = fetch_weather_context(city) if df is None: return f"ā Data fetch failed for {city}", None, None, gr.update(), "Temperature" # --- STEP A: DYNAMIC COLUMN MAPPING --- col_map = { 'Temperature': 'temperature_2m', 'Temp': 'temperature_2m', 'temperature': 'temperature_2m', 'temperature_2m': 'temperature_2m', 'Humidity': 'relative_humidity_2m', 'relative_humidity': 'relative_humidity_2m', 'Pressure': 'pressure_msl', 'pressure': 'pressure_msl', 'Wind': 'wind_speed_10m', 'Wind Speed': 'wind_speed_10m', 'wind_speed': 'wind_speed_10m' } ordered_columns = [] target_idx = 0 for idx, feat in enumerate(FEATURES): feat_lower = feat.lower() matched_col = None for k, v in col_map.items(): if k.lower() in feat_lower: matched_col = v break if matched_col: ordered_columns.append(matched_col) if 'temp' in feat_lower: target_idx = idx else: ordered_columns.append('temperature_2m') # 2. Extract Data raw_data = df[ordered_columns].values X_scaled = feature_scaler.transform(raw_data) current_input = torch.FloatTensor(X_scaled).unsqueeze(0).to(DEVICE) last_time = df.iloc[-1]['time'] current_vals = df.iloc[-1][ordered_columns].values future_timestamps = [] future_predictions_actual = [] # 3. Autoregressive Loop with torch.no_grad(): for i in range(int(hours_ahead)): y_next_scaled = model(current_input) y_np = y_next_scaled.cpu().numpy() last_input_step = current_input[0, -1, :].cpu().numpy() new_step_scaled = last_input_step.copy() if y_np.size == 1: new_step_scaled[target_idx] = y_np.item() else: new_step_scaled = y_np.reshape(len(FEATURES)) new_step_scaled = new_step_scaled.reshape(1, len(FEATURES)) pred_actual = feature_scaler.inverse_transform(new_step_scaled)[0] future_predictions_actual.append(pred_actual) next_step_tensor = torch.FloatTensor(new_step_scaled).unsqueeze(0).to(DEVICE) current_input = torch.cat((current_input[:, 1:, :], next_step_tensor), dim=1) last_time += timedelta(hours=1) future_timestamps.append(format_local_datetime(last_time)) # 4. Post-Process predictions_actual = np.array(future_predictions_actual) # --- LOGIC FIX: Select Correct Prediction for Display --- if int(hours_ahead) > 1: # For long forecasts, show the LAST predicted value (e.g., value at hour 12) display_pred = predictions_actual[-1] time_label = f"+{int(hours_ahead)} Hours" display_feats = ['Temperature'] grid_cols = "1fr" else: # For 1h, show the IMMEDIATE next value display_pred = predictions_actual[0] time_label = "Next Hour" display_feats = ['Temperature', 'Humidity', 'Pressure', 'Wind'] grid_cols = "1fr 1fr 1fr 1fr" # --- DISPLAY CARDS LOGIC --- pred_dict = {feat: val for feat, val in zip(FEATURES, display_pred)} curr_dict = {feat: val for feat, val in zip(FEATURES, current_vals)} units = {'Temperature': '°C', 'Humidity': '%', 'Pressure': 'hPa', 'Wind': 'km/h'} colors = {'Temperature': '#ff6b6b', 'Humidity': '#4dabf7', 'Pressure': '#ffd43b', 'Wind': '#51cf66'} icons = {'Temperature': 'š”ļø', 'Humidity': 'š§', 'Pressure': 'š§', 'Wind': 'šØ'} cards_html = f"""
Real-time AI Forecasting for Dubai & Riyadh