import gradio as gr import requests import pandas as pd import os import pathlib import zipfile import shutil from datetime import datetime from huggingface_hub import hf_hub_download, snapshot_download from autogluon.tabular import TabularPredictor # Team name mappings # ESPN uses different abbreviations than NFL data ESPN_TEAM_MAP = { "WAS": "WSH", # Washington "LA": "LAR", # LA Rams "LAR": "LAR", # LA Rams (if already normalized) } # Reverse mapping for ESPN to internal ESPN_TO_INTERNAL = { "WSH": "WAS", "LAR": "LAR", } def normalize_team_for_espn(team_abbr): """Convert internal team abbreviation to ESPN format.""" return ESPN_TEAM_MAP.get(team_abbr, team_abbr) def normalize_team_from_espn(team_abbr): """Convert ESPN team abbreviation to internal format.""" return ESPN_TO_INTERNAL.get(team_abbr, team_abbr) # --- Download Model and Embeddings --- def download_model_and_embeddings(MODEL_REPO_ID="SebastianAndreu/2025-24679-NFL-Yards-Predictor"): try: # Configuration MODEL_REPO_ID = "SebastianAndreu/2025-24679-NFL-Yards-Predictor" ZIP_FILENAME = "autogluon_predictor_dir.zip" CACHE_DIR = pathlib.Path("hf_assets") EXTRACT_DIR = CACHE_DIR / "predictor_native" MODEL_LOCAL_DIR = "nfl_model" # Download & load the native predictor def _prepare_predictor_dir() -> str: CACHE_DIR.mkdir(parents=True, exist_ok=True) local_zip = hf_hub_download( repo_id=MODEL_REPO_ID, filename=ZIP_FILENAME, repo_type="model", local_dir=str(CACHE_DIR), local_dir_use_symlinks=False, ) if EXTRACT_DIR.exists(): shutil.rmtree(EXTRACT_DIR) EXTRACT_DIR.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(local_zip, "r") as zf: zf.extractall(str(EXTRACT_DIR)) contents = list(EXTRACT_DIR.iterdir()) predictor_root = contents[0] if (len(contents) == 1 and contents[0].is_dir()) else EXTRACT_DIR return str(predictor_root) # Download model and embeddings try: # Download the model and data using snapshot_download model_path = snapshot_download( repo_id=MODEL_REPO_ID, repo_type="model", local_dir=MODEL_LOCAL_DIR, local_dir_use_symlinks=False ) # Load the predictor PREDICTOR_DIR = _prepare_predictor_dir() PREDICTOR = TabularPredictor.load(PREDICTOR_DIR, require_py_version_match=False) # Load embeddings emb_df = pd.read_csv(os.path.join(MODEL_LOCAL_DIR, "data", "player_historical_embeddings.csv")) except Exception as e: print(f"Error loading model or embeddings: {e}") import traceback traceback.print_exc() PREDICTOR = None emb_df = None return PREDICTOR, emb_df except Exception as e: print(f"Error loading model: {e}") import traceback traceback.print_exc() return None, None # Load model at startup predictor, player_embeddings = download_model_and_embeddings() # Load player and game data try: players_df = pd.read_csv("players.csv") games_df = pd.read_csv("games.csv") # Normalize team names in games_df to match internal format if 'home_team' in games_df.columns: games_df['home_team'] = games_df['home_team'].replace({'LA': 'LAR'}) if 'away_team' in games_df.columns: games_df['away_team'] = games_df['away_team'].replace({'LA': 'LAR'}) # Normalize team names in players_df if 'latest_team' in players_df.columns: players_df['latest_team'] = players_df['latest_team'].replace({'LA': 'LAR'}) receivers_df = players_df[ (players_df['position'].isin(['WR', 'TE', 'RB'])) & (players_df['ngs_status'] == 'ACT') ].copy() receiver_choices = sorted(receivers_df['display_name'].dropna().unique().tolist()) passers_df = players_df[ (players_df['position'] == 'QB') & (players_df['status'] == 'ACT') ].copy() passer_choices = sorted(passers_df['display_name'].dropna().unique().tolist()) except Exception as e: print(f"Error loading player/game data: {e}") players_df = pd.DataFrame() games_df = pd.DataFrame() receiver_choices = [] passer_choices = [] # Stadium coordinates STADIUM_COORDS = { "ARI": {"lat": 33.5276, "lon": -112.2626}, "ATL": {"lat": 33.7554, "lon": -84.4008}, "BAL": {"lat": 39.2780, "lon": -76.6227}, "BUF": {"lat": 42.7738, "lon": -78.7870}, "CAR": {"lat": 35.2258, "lon": -80.8528}, "CHI": {"lat": 41.8623, "lon": -87.6167}, "CIN": {"lat": 39.0954, "lon": -84.5160}, "CLE": {"lat": 41.5061, "lon": -81.6995}, "DAL": {"lat": 32.7473, "lon": -97.0945}, "DEN": {"lat": 39.7439, "lon": -105.0201}, "DET": {"lat": 42.3400, "lon": -83.0456}, "GB": {"lat": 44.5013, "lon": -88.0622}, "HOU": {"lat": 29.6847, "lon": -95.4107}, "IND": {"lat": 39.7601, "lon": -86.1639}, "JAX": {"lat": 30.3239, "lon": -81.6373}, "KC": {"lat": 39.0489, "lon": -94.4839}, "LV": {"lat": 36.0908, "lon": -115.1833}, "LAC": {"lat": 33.9535, "lon": -118.3390}, "LAR": {"lat": 33.9535, "lon": -118.3390}, "MIA": {"lat": 25.9580, "lon": -80.2389}, "MIN": {"lat": 44.9738, "lon": -93.2577}, "NE": {"lat": 42.0909, "lon": -71.2643}, "NO": {"lat": 29.9511, "lon": -90.0812}, "NYG": {"lat": 40.8128, "lon": -74.0742}, "NYJ": {"lat": 40.8128, "lon": -74.0742}, "PHI": {"lat": 39.9008, "lon": -75.1675}, "PIT": {"lat": 40.4468, "lon": -80.0158}, "SF": {"lat": 37.4032, "lon": -121.9698}, "SEA": {"lat": 47.5952, "lon": -122.3316}, "TB": {"lat": 27.9759, "lon": -82.5033}, "TEN": {"lat": 36.1665, "lon": -86.7713}, "WAS": {"lat": 38.9076, "lon": -76.8645} } def get_player_info(player_name, players_df): """Get player's gsis_id, latest team, and headshot from display_name.""" player_row = players_df[players_df['display_name'] == player_name] if player_row.empty: return None, None, None return player_row.iloc[0]['gsis_id'], player_row.iloc[0]['latest_team'], player_row.iloc[0].get('headshot', None) def update_receiver_image(receiver_name): """Update receiver headshot when selection changes.""" if not receiver_name or players_df.empty: return None _, _, headshot = get_player_info(receiver_name, players_df) return headshot def update_passer_image(passer_name): """Update passer headshot when selection changes.""" if not passer_name or players_df.empty: return None _, _, headshot = get_player_info(passer_name, players_df) return headshot def get_game_info(receiver_team, season, week, games_df): """Get game information based on receiver's team, season, and week.""" # Ensure receiver_team is normalized (e.g., LAR not LA) receiver_team_normalized = receiver_team.replace('LA', 'LAR') if receiver_team == 'LA' else receiver_team game_row = games_df[ (games_df['season'] == season) & (games_df['week'] == week) & ((games_df['home_team'] == receiver_team_normalized) | (games_df['away_team'] == receiver_team_normalized)) ] if game_row.empty: print(f"⚠ No game found for team '{receiver_team_normalized}' in season {season}, week {week}") return None game = game_row.iloc[0] is_home = game['home_team'] == receiver_team_normalized print(f"✓ Found game: {game['away_team']} @ {game['home_team']}") print(f" Receiver team '{receiver_team_normalized}' is {'HOME' if is_home else 'AWAY'}") return { 'home_team': game['home_team'], 'away_team': game['away_team'], 'receiver_is_home': is_home, 'opponent_team': game['away_team'] if is_home else game['home_team'], 'surface': game.get('surface', 'grass'), 'roof': game.get('roof', 'outdoors'), 'gameday': game.get('gameday'), 'gametime': game.get('gametime') } def get_weather_forecast(home_team, game_datetime): """Get weather forecast for a stadium at game time.""" coords = STADIUM_COORDS.get(home_team) if not coords: return None url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": coords["lat"], "longitude": coords["lon"], "hourly": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code", "temperature_unit": "fahrenheit", "wind_speed_unit": "mph", "timezone": "America/New_York" } try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() hourly = data.get("hourly", {}) times = hourly.get("time", []) game_time_str = game_datetime.strftime("%Y-%m-%dT%H:%M") closest_idx = 0 for i, time_str in enumerate(times): if time_str >= game_time_str: closest_idx = i break temp = hourly["temperature_2m"][closest_idx] humidity = hourly["relative_humidity_2m"][closest_idx] wind = hourly["wind_speed_10m"][closest_idx] weather_code = hourly["weather_code"][closest_idx] is_rain = weather_code in [51, 53, 55, 61, 63, 65, 80, 81, 82] is_snow = weather_code in [71, 73, 75, 77, 85, 86] is_clear = weather_code in [0, 1, 2] return { "temp_f": temp, "humidity_pct": humidity, "wind_mph": wind, "is_rain": int(is_rain), "is_snow": int(is_snow), "is_clear": int(is_clear) } except Exception as e: print(f"⚠ Weather API error: {e}") return None def get_game_info_espn(home_team, away_team, week): """Get game time, spread, and total from ESPN API.""" # Convert internal team names to ESPN format home_team_espn = normalize_team_for_espn(home_team) away_team_espn = normalize_team_for_espn(away_team) print(f"\n=== Querying ESPN API ===") print(f"Internal format: {away_team} @ {home_team}") print(f"ESPN format: {away_team_espn} @ {home_team_espn}") result = { "game_datetime": None, "pregame_spread": 0, "pregame_total": 0 } try: # Try both regular season (2) and postseason (3) for season_type in [2, 3]: url = f"https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?seasontype={season_type}&week={week}" response = requests.get(url, timeout=10) response.raise_for_status() data = response.json() for event in data.get('events', []): competition = event.get('competitions', [{}])[0] competitors = competition.get('competitors', []) if len(competitors) < 2: continue # ESPN puts home team at index 0, away team at index 1 espn_home = competitors[0]['team']['abbreviation'] espn_away = competitors[1]['team']['abbreviation'] print(f" Checking ESPN game: {espn_away} @ {espn_home}") # Match using ESPN format if espn_home == home_team_espn and espn_away == away_team_espn: print(f" ✓ MATCH FOUND!") # Get game time game_date_str = event.get('date') if game_date_str: result["game_datetime"] = datetime.fromisoformat(game_date_str.replace('Z', '+00:00')) print(f" Game time: {result['game_datetime']}") # Get odds from competition odds_data = competition.get('odds', []) if odds_data and len(odds_data) > 0: spread = odds_data[0].get('spread') total = odds_data[0].get('overUnder') # ESPN's spread is from home team perspective result["pregame_spread"] = float(spread) if spread is not None else 0 result["pregame_total"] = float(total) if total is not None else 0 print(f" Spread: {result['pregame_spread']} (home perspective)") print(f" Total: {result['pregame_total']}") else: print(f" ⚠ No odds data available") return result print(f" ✗ No matching game found in ESPN API") except Exception as e: print(f"⚠ ESPN API error: {e}") import traceback traceback.print_exc() return result def predict_yards(model_input_dict, receiver_id, passer_id): """Make yards prediction using the loaded model.""" if predictor is None: print("ERROR: Predictor is None") return None, "Model not loaded" try: print(f"\n=== Starting Prediction ===") print(f"Receiver ID: {receiver_id}") print(f"Passer ID: {passer_id}") print(f"Model input dict: {model_input_dict}") input_data = { "receiver_player_id": [str(receiver_id)], "defteam": [int(model_input_dict["defteam"])], "posteam": [int(model_input_dict["posteam"])], "surface": [int(model_input_dict["surface"])], "is_dome": [int(model_input_dict["is_dome"])], "is_rain": [int(model_input_dict["is_rain"])], "is_snow": [int(model_input_dict["is_snow"])], "is_clear": [int(model_input_dict["is_clear"])], "temp_f": [float(model_input_dict["temp_f"])], "humidity_pct": [float(model_input_dict["humidity_pct"])], "wind_mph": [float(model_input_dict["wind_mph"])], "home_team": [int(model_input_dict["home_team"])], "away_team": [int(model_input_dict["away_team"])], "pregame_spread": [float(model_input_dict["pregame_spread"])], "pregame_total": [float(model_input_dict["pregame_total"])], "passer_player_id": [str(passer_id)] } input_df = pd.DataFrame(input_data) print(f"\nInitial DataFrame shape: {input_df.shape}") print(f"Initial columns: {list(input_df.columns)}") # Check what columns the predictor expects try: print(f"\nPredictor feature metadata:") print(f"Features: {predictor.feature_metadata}") except: print("Could not get feature metadata") if player_embeddings is not None: print(f"\nProcessing embeddings...") emb_df = player_embeddings.copy() emb_df['player_id'] = emb_df['player_id'].astype(str) print(f"Embeddings shape: {emb_df.shape}") print(f"Looking for receiver '{receiver_id}' in embeddings") print(f"Receiver ID in embeddings: {str(receiver_id) in emb_df['player_id'].values}") input_df = input_df.merge( emb_df, left_on="receiver_player_id", right_on="player_id", how="left" ).drop(columns=["player_id"], errors="ignore") print(f"After merge shape: {input_df.shape}") emb_cols = [c for c in emb_df.columns if c.startswith("emb_")] print(f"Number of embedding columns: {len(emb_cols)}") # Check for missing embeddings missing_emb = input_df[emb_cols].isna().any().any() print(f"Has missing embeddings: {missing_emb}") if missing_emb: print("Filling missing embeddings with mean...") mean_emb = emb_df[emb_cols].mean() input_df[emb_cols] = input_df[emb_cols].fillna(mean_emb) print(f"\nFinal DataFrame info:") print(f"Shape: {input_df.shape}") print(f"Columns ({len(input_df.columns)}): {list(input_df.columns)}") print(f"Data types:\n{input_df.dtypes}") print(f"\nFirst row values:") for col in input_df.columns: print(f" {col}: {input_df[col].iloc[0]}") # Try prediction try: print("\n--- Attempting prediction with default model ---") prediction = predictor.predict(input_df) yards = float(prediction.values[0]) print(f"✓ SUCCESS! Predicted yards: {yards}") return yards, None except Exception as e: print(f"\n✗ Default prediction FAILED") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("Full traceback:") traceback.print_exc() # Try with specific model try: print("\n--- Attempting prediction with best model from leaderboard ---") leaderboard = predictor.leaderboard(silent=True) print(f"Available models:\n{leaderboard[['model', 'score_val']]}") best_model = leaderboard.iloc[0]['model'] print(f"Using model: {best_model}") prediction = predictor.predict(input_df, model=best_model) yards = float(prediction.values[0]) print(f"✓ SUCCESS with {best_model}! Predicted yards: {yards}") return yards, None except Exception as e2: print(f"\n✗ Leaderboard prediction ALSO FAILED") print(f"Error type: {type(e2).__name__}") print(f"Error message: {str(e2)}") import traceback print("Full traceback:") traceback.print_exc() return None, f"Both prediction attempts failed: {str(e2)}" except Exception as e: print(f"\n✗✗✗ OUTER EXCEPTION ✗✗✗") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("Full traceback:") traceback.print_exc() return None, f"Prediction setup error: {str(e)}" def create_model_input_and_predict(receiver_name, passer_name, week, season): """Create model input from user selections and make prediction.""" try: # Get receiver info (posteam from latest_team) receiver_id, receiver_team, receiver_headshot = get_player_info(receiver_name, players_df) if receiver_id is None: return "❌ Prediction Failed", f"Could not find receiver '{receiver_name}'", f"❌ Error: Could not find receiver '{receiver_name}' in database", None, None # Get passer info passer_id, passer_team, passer_headshot = get_player_info(passer_name, players_df) if passer_id is None: return "❌ Prediction Failed", f"Could not find passer '{passer_name}'", f"❌ Error: Could not find passer '{passer_name}' in database", None, None print(f"\n=== Game Lookup ===") print(f"Receiver: {receiver_name} (Team: {receiver_team})") print(f"Season: {season}, Week: {week}") # Get game info from games.csv using receiver's team game_info = get_game_info(receiver_team, season, week, games_df) if game_info is None: return "❌ Prediction Failed", "Game not found", f"❌ Error: Could not find game for {receiver_team} in Week {week} of {season} season", None, None home_team = game_info['home_team'] away_team = game_info['away_team'] opponent_team = game_info['opponent_team'] receiver_is_home = game_info['receiver_is_home'] # Get game info from ESPN (including spread and total) espn_info = get_game_info_espn(home_team, away_team, week) if espn_info["game_datetime"]: game_datetime = espn_info["game_datetime"] elif game_info.get('gameday') and game_info.get('gametime'): try: game_datetime = datetime.strptime( f"{game_info['gameday']} {game_info['gametime']}", "%Y-%m-%d %H:%M:%S" ) except: game_datetime = None else: game_datetime = None weather = None if game_datetime: weather = get_weather_forecast(home_team, game_datetime) dome_teams = ["ARI", "ATL", "DAL", "DET", "HOU", "IND", "LV", "LAR", "LAC", "MIN", "NO"] is_dome = home_team in dome_teams or game_info.get('roof') == 'dome' if weather: game_data = { "temp_f": weather["temp_f"], "humidity_pct": weather["humidity_pct"], "wind_mph": weather["wind_mph"], "is_dome": int(is_dome), "is_rain": weather["is_rain"] if not is_dome else 0, "is_snow": weather["is_snow"] if not is_dome else 0, "is_clear": weather["is_clear"] if not is_dome else 0 } else: game_data = { "temp_f": 72 if is_dome else 70, "humidity_pct": 50, "wind_mph": 0 if is_dome else 5, "is_dome": int(is_dome), "is_rain": 0, "is_snow": 0, "is_clear": 0 if is_dome else 1 } team_map = { "ARI": 1, "ATL": 2, "BAL": 3, "BUF": 4, "CAR": 5, "CHI": 6, "CIN": 7, "CLE": 8, "DAL": 9, "DEN": 10, "DET": 11, "GB": 12, "HOU": 13, "IND": 14, "JAX": 15, "KC": 16, "LV": 17, "LAC": 18, "LAR": 19, "MIA": 20, "MIN": 21, "NE": 22, "NO": 23, "NYG": 24, "NYJ": 25, "PHI": 26, "PIT": 27, "SEA": 28, "SF": 29, "TB": 30, "TEN": 31, "WAS": 32 } surface_map = { "a_turf": 1, "grass": 2, "sportturf": 3, "fieldturf": 4, "matrixturf": 5, "astroturf": 6, "0": 0 } posteam_id = team_map.get(receiver_team, 0) defteam_id = team_map.get(opponent_team, 0) home_team_id = team_map.get(home_team, 0) away_team_id = team_map.get(away_team, 0) surface_type = game_info.get('surface', 'grass') surface_id = surface_map.get(surface_type.lower() if surface_type else 'grass', 2) # Use spread and total from ESPN API # ESPN spread is from HOME team perspective # MODEL ALWAYS USES AWAY TEAM PERSPECTIVE espn_spread_home = espn_info["pregame_spread"] pregame_total = espn_info["pregame_total"] # Convert to away team perspective for the model # If home is -7.5, away is +7.5 # If home is +3, away is -3 pregame_spread = -espn_spread_home print(f"\n=== Spread Conversion ===") print(f"ESPN spread (home team {home_team} perspective): {espn_spread_home}") print(f"Converted to away team ({away_team}) perspective: {pregame_spread}") print(f"Receiver team: {receiver_team} ({'HOME' if receiver_is_home else 'AWAY'})") print(f"Model Input Spread: {pregame_spread} (always from {away_team}'s perspective)") model_input = { "receiver_player_id": receiver_id, "defteam": defteam_id, "posteam": posteam_id, "surface": surface_id, "is_dome": game_data["is_dome"], "is_rain": game_data["is_rain"], "is_snow": game_data["is_snow"], "is_clear": game_data["is_clear"], "temp_f": game_data["temp_f"], "humidity_pct": game_data["humidity_pct"], "wind_mph": game_data["wind_mph"], "home_team": home_team_id, "away_team": away_team_id, "pregame_spread": pregame_spread, "pregame_total": pregame_total, "passer_player_id": passer_id } predicted_yards, error = predict_yards(model_input, receiver_id, passer_id) if error: prediction_text = "❌ Prediction Failed" predicted_value = f"Error: {error}" elif predicted_yards is not None: prediction_text = "🎯 PREDICTION" predicted_value = f"{predicted_yards:.1f} yards" else: prediction_text = "⚠️ Unavailable" predicted_value = "Prediction unavailable" # Calculate receiver's team spread for display purposes if receiver_is_home: receiver_team_spread = espn_spread_home else: receiver_team_spread = pregame_spread output = f""" 🏈 **Game Information:** • Matchup: {away_team} @ {home_team} (Week {week}, {season}) • Game Time: {game_datetime if game_datetime else 'TBD'} • Venue: {home_team} ({surface_type}, {'Indoor' if is_dome else 'Outdoor'}) 👤 **Players:** • Receiver: {receiver_name} (ID: {receiver_id}) - Team: {receiver_team} • Passer: {passer_name} (ID: {passer_id}) - Team: {passer_team} • Opponent: {opponent_team} • Playing {'Home' if receiver_is_home else 'Away'} 🌤️ **Weather Conditions:** • Temperature: {game_data['temp_f']}°F • Humidity: {game_data['humidity_pct']}% • Wind: {game_data['wind_mph']} mph • Conditions: {'Dome' if is_dome else 'Rain' if game_data['is_rain'] else 'Snow' if game_data['is_snow'] else 'Clear'} 💰 **Betting Lines:** • Spread: {receiver_team} {receiver_team_spread if receiver_team_spread != 0 else 'N/A'} • Total: {pregame_total if pregame_total != 0 else 'N/A'} """ return prediction_text, predicted_value, output, receiver_headshot, passer_headshot except Exception as e: import traceback traceback.print_exc() return "❌ Error", f"{str(e)}", f"❌ Error: {str(e)}", None, None # Create Gradio interface with gr.Blocks(title="NFL Receiver Yards Predictor", theme=gr.themes.Soft()) as app: gr.Markdown("# 🏈 NFL Receiver Yards Predictor") gr.Markdown("Predict receiving yards with AI-powered analysis. Just select the player, QB, week, and season!") with gr.Row(): with gr.Column(scale=4): receiver_name = gr.Dropdown(choices=receiver_choices, label="🎯 Receiver Name", value="", allow_custom_value=False) with gr.Column(scale=1): receiver_img = gr.Image(label="", show_label=False, height=120, show_download_button=False, container=False) with gr.Row(): with gr.Column(scale=4): passer_name = gr.Dropdown(choices=passer_choices, label="🏈 Passer Name", value="", allow_custom_value=False) with gr.Column(scale=1): passer_img = gr.Image(label="", show_label=False, height=120, show_download_button=False, container=False) with gr.Row(): week = gr.Number(label="📅 Week", value=6, precision=0) season = gr.Number(label="📅 Season", value=2025, precision=0) predict_btn = gr.Button("🔮 Predict Yards", variant="primary", size="lg") with gr.Row(): with gr.Column(): prediction_label = gr.Textbox(label="", show_label=False, interactive=False, container=False, lines=1, max_lines=1, text_align="center") with gr.Column(): prediction_value = gr.Textbox(label="", show_label=False, interactive=False, container=False, lines=1, max_lines=1, text_align="center") output = gr.Textbox(label="📊 Detailed Results", lines=15, max_lines=20) receiver_name.change( fn=update_receiver_image, inputs=[receiver_name], outputs=[receiver_img] ) passer_name.change( fn=update_passer_image, inputs=[passer_name], outputs=[passer_img] ) predict_btn.click( fn=create_model_input_and_predict, inputs=[receiver_name, passer_name, week, season], outputs=[prediction_label, prediction_value, output, receiver_img, passer_img] ) gr.Markdown(""" ### 📋 How It Works: 1. **Select Receiver** → Headshot appears instantly 2. **Select Passer (QB)** → Headshot appears instantly 3. **Enter Week & Season** 4. **Click "Predict Yards"** → Get your AI-powered prediction! ### ⚡ What Happens Automatically: - 🖼️ Player headshots load in real-time as you select them - 🏟️ Determines matchup and venue based on receiver's team schedule - 🌤️ Fetches live weather forecast for game time - 💰 Loads real-time betting lines (spread & total) from ESPN API - 🤖 Generates AI prediction using advanced machine learning model - 📊 Displays comprehensive game analysis and prediction results ### 🔧 Technical Notes: - Team abbreviations are automatically normalized (WAS/WSH, LA/LAR) - Home/away teams correctly identified from games.csv - ESPN API queries use proper team format for accurate odds retrieval """) if __name__ == "__main__": app.launch(share=True, debug=True)