File size: 29,435 Bytes
d2d1682
 
 
 
cb4b023
 
 
d2d1682
abc7edd
d2d1682
 
abc7edd
625be8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2d1682
cb4b023
d2d1682
abc7edd
cb4b023
 
abc7edd
cb4b023
abc7edd
cb4b023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abc7edd
d2d1682
cb4b023
abc7edd
 
d2d1682
 
 
 
 
0340143
 
 
 
5eb14f6
625be8d
 
 
 
 
 
 
 
 
 
0340143
6fd471d
c14d3b4
0340143
 
5eb14f6
0340143
 
 
 
 
5eb14f6
0340143
625be8d
0340143
 
 
 
d2d1682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb4b023
 
 
 
 
 
 
0340143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625be8d
 
 
0340143
 
 
625be8d
0340143
 
 
625be8d
0340143
 
 
625be8d
 
 
 
0340143
 
 
 
 
 
 
 
 
2aa7019
0340143
 
d2d1682
 
 
 
 
0340143
d2d1682
 
 
 
 
 
 
0340143
d2d1682
 
 
 
0340143
d2d1682
 
0340143
d2d1682
 
 
 
 
 
0340143
d2d1682
 
 
 
0340143
d2d1682
 
 
0340143
d2d1682
 
 
 
 
625be8d
d2d1682
 
 
2aa7019
625be8d
 
 
 
 
 
 
 
2aa7019
 
 
 
 
0340143
d2d1682
2aa7019
d2d1682
 
 
 
 
0340143
d2d1682
 
 
0340143
d2d1682
 
0340143
625be8d
 
 
 
 
0340143
625be8d
 
 
 
2aa7019
d2d1682
 
 
625be8d
2aa7019
 
 
 
 
 
 
 
 
 
625be8d
 
 
 
 
2aa7019
d2d1682
 
625be8d
 
0340143
2aa7019
625be8d
 
d2d1682
0340143
d2d1682
 
 
 
a4a7681
d2d1682
0340143
d2d1682
a4a7681
 
 
 
 
0340143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2d1682
0340143
a4a7681
 
 
 
 
 
 
 
 
0340143
d2d1682
a4a7681
0340143
 
 
a4a7681
 
 
 
d2d1682
0340143
d2d1682
 
 
 
0340143
a4a7681
 
0340143
a4a7681
 
 
 
 
 
 
 
0340143
d2d1682
abc7edd
a4a7681
 
 
 
 
 
 
 
 
d2d1682
a4a7681
d2d1682
 
a4a7681
abc7edd
a4a7681
 
 
 
 
 
 
 
 
 
cb4b023
a4a7681
cb4b023
a4a7681
cb4b023
a4a7681
 
cb4b023
 
a4a7681
cb4b023
a4a7681
 
 
 
 
 
 
 
 
cb4b023
d2d1682
a4a7681
 
 
 
 
 
 
d2d1682
0340143
d2d1682
 
625be8d
0340143
d2d1682
cb4b023
0340143
625be8d
0340143
d2d1682
cb4b023
d2d1682
625be8d
 
 
 
 
0340143
 
cb4b023
0340143
 
 
 
 
 
2aa7019
0340143
d2d1682
0340143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2d1682
 
 
 
 
 
0340143
d2d1682
0340143
d2d1682
 
 
 
 
 
 
 
cb4b023
 
0340143
625be8d
 
007aa04
625be8d
2aa7019
625be8d
007aa04
 
 
 
625be8d
 
007aa04
 
625be8d
007aa04
0340143
d2d1682
 
 
0340143
d2d1682
0340143
 
 
 
 
 
 
d2d1682
 
0340143
 
 
d2d1682
0340143
d2d1682
0340143
d2d1682
0340143
 
d2d1682
0340143
 
d2d1682
0340143
 
d2d1682
007aa04
 
 
 
 
 
0340143
 
d2d1682
0340143
 
 
 
 
 
 
 
 
 
 
 
007aa04
 
 
d2d1682
0340143
 
 
d2d1682
625be8d
 
0340143
d2d1682
0340143
d2d1682
0340143
 
 
d2d1682
0340143
cb4b023
0340143
 
d2d1682
0340143
 
cb4b023
0340143
 
 
 
 
 
 
 
 
d2d1682
 
0340143
 
 
d2d1682
0340143
 
 
 
 
 
 
 
 
 
 
d2d1682
0340143
 
 
 
 
 
d2d1682
 
0340143
 
d2d1682
0340143
d2d1682
cb4b023
 
 
 
 
 
 
 
 
2aa7019
cb4b023
 
625be8d
 
 
 
 
d2d1682
 
 
abc7edd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
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)