Spaces:
No application file
No application file
add highlights/timeline for games
Browse files
api/scripts/create_game_highlights.py
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from openai import OpenAI
|
2 |
+
import base64
|
3 |
+
from model_player import Player
|
4 |
+
import json
|
5 |
+
|
6 |
+
client = OpenAI()
|
7 |
+
|
8 |
+
|
9 |
+
def get_ai_response(prompt):
|
10 |
+
response = client.responses.create(
|
11 |
+
model="gpt-4o",
|
12 |
+
input=prompt
|
13 |
+
)
|
14 |
+
return response.output_text
|
15 |
+
|
16 |
+
|
17 |
+
def player_to_dict(player):
|
18 |
+
return {
|
19 |
+
"name": player.name,
|
20 |
+
"position": player.position,
|
21 |
+
"shirt_number": player.shirt_number,
|
22 |
+
"preferred_foot": player.preferred_foot,
|
23 |
+
"role": player.role,
|
24 |
+
"form": player.form,
|
25 |
+
}
|
26 |
+
|
27 |
+
|
28 |
+
prompt = """
|
29 |
+
You are simulating a fictional soccer match and creating a realistic event timeline from kickoff to final whistle.
|
30 |
+
|
31 |
+
Here is the match context:
|
32 |
+
- Match type: {match_type}
|
33 |
+
- Location: {location}
|
34 |
+
- Date: {date}
|
35 |
+
- Home team: {home_team}
|
36 |
+
- Away team: {away_team}
|
37 |
+
- Winner: {winner}
|
38 |
+
- Score: {score}
|
39 |
+
|
40 |
+
Here is the {home_team} roster:
|
41 |
+
{home_roster}
|
42 |
+
|
43 |
+
Here is the {away_team} roster:
|
44 |
+
{away_roster}
|
45 |
+
|
46 |
+
Rules:
|
47 |
+
- Spread out events across 90+ minutes
|
48 |
+
- Substitutions should use players not in the starting 11 if available
|
49 |
+
- Vary event types and timing
|
50 |
+
|
51 |
+
Only return a JSON array of chronological match events, formatted exactly like this example:
|
52 |
+
|
53 |
+
[
|
54 |
+
{{
|
55 |
+
"minute": // string, e.g. 45+2
|
56 |
+
"team": // home or away team name
|
57 |
+
"event": // e.g. Goal, Yellow Card, Substitution, etc.
|
58 |
+
"player": // name + number
|
59 |
+
"description": // 1-sentence summary
|
60 |
+
}}
|
61 |
+
]
|
62 |
+
"""
|
63 |
+
|
64 |
+
|
65 |
+
# # match 1
|
66 |
+
# match_type = "semifinal 1"
|
67 |
+
# location = "El Templo del Sol (fictional stadium in Mérida, Mexico)"
|
68 |
+
# date = "July 10, 2025"
|
69 |
+
# home_team = Player.teams[2]
|
70 |
+
# away_team = Player.teams[3]
|
71 |
+
# winner = Player.teams[2]
|
72 |
+
# score = "2–1"
|
73 |
+
# home_roster = None
|
74 |
+
# away_roster = None
|
75 |
+
|
76 |
+
|
77 |
+
# match 2
|
78 |
+
match_type = "semifinal 2"
|
79 |
+
location = "Everglade Arena (fictional stadium in Miami, Florida)"
|
80 |
+
date = "July 11, 2025"
|
81 |
+
home_team = Player.teams[1]
|
82 |
+
away_team = Player.teams[0]
|
83 |
+
winner = Player.teams[1]
|
84 |
+
score = "3-3 (4-2 pens)"
|
85 |
+
home_roster = None
|
86 |
+
away_roster = None
|
87 |
+
|
88 |
+
home_players = Player.get_players(team=home_team)
|
89 |
+
away_players = Player.get_players(team=away_team)
|
90 |
+
|
91 |
+
home_roster = [player_to_dict(player) for player in home_players]
|
92 |
+
away_roster = [player_to_dict(player) for player in away_players]
|
93 |
+
|
94 |
+
prompt = prompt.format(
|
95 |
+
match_type=match_type,
|
96 |
+
location=location,
|
97 |
+
date=date,
|
98 |
+
home_team=home_team,
|
99 |
+
away_team=away_team,
|
100 |
+
home_roster=home_roster,
|
101 |
+
away_roster=away_roster,
|
102 |
+
winner=winner,
|
103 |
+
score=score,
|
104 |
+
)
|
105 |
+
|
106 |
+
# print(prompt)
|
107 |
+
|
108 |
+
response = get_ai_response(prompt)
|
109 |
+
|
110 |
+
# Parse the AI response as JSON
|
111 |
+
try:
|
112 |
+
if response.startswith("```json") and response.endswith("```"):
|
113 |
+
response = response[7:-3]
|
114 |
+
events = json.loads(response)
|
115 |
+
except Exception as e:
|
116 |
+
print("Error parsing AI response as JSON:", e)
|
117 |
+
print("Raw response was:\n", response)
|
118 |
+
raise
|
119 |
+
|
120 |
+
# Pretty-print JSON to terminal
|
121 |
+
print(json.dumps(events, indent=4, sort_keys=True))
|
122 |
+
|
123 |
+
# Write pretty JSON to file
|
124 |
+
with open(f"/workspace/data/huge-league/games/{match_type.replace(' ', '_')}.json", "w") as f:
|
125 |
+
json.dump(events, f, indent=4, sort_keys=True)
|
126 |
+
|
127 |
+
# Optionally, if you want a more human-readable text format, uncomment below:
|
128 |
+
# def pretty_print_events(events):
|
129 |
+
# for event in events:
|
130 |
+
# print(f"[{event['minute']}] {event['team']} - {event['event']} - {event['player']}: {event['description']}")
|
131 |
+
# pretty_print_events(events)
|
132 |
+
|
api/scripts/model_player.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
import os
|
2 |
import json
|
3 |
from pydantic import BaseModel, Field, ValidationError
|
4 |
-
from typing import Literal, Optional
|
5 |
import hashlib
|
6 |
|
7 |
|
8 |
class Player(BaseModel):
|
|
|
9 |
number: int
|
10 |
name: str
|
11 |
age: int
|
@@ -87,9 +88,12 @@ class Player(BaseModel):
|
|
87 |
return cls.model_validate(data)
|
88 |
|
89 |
@classmethod
|
90 |
-
def get_players(cls):
|
91 |
for filename in os.listdir("/workspace/data/huge-league/players"):
|
92 |
-
|
|
|
|
|
|
|
93 |
|
94 |
def save_image(self, image_bytes):
|
95 |
filename = self.filename.replace(".json", ".png")
|
|
|
1 |
import os
|
2 |
import json
|
3 |
from pydantic import BaseModel, Field, ValidationError
|
4 |
+
from typing import Literal, Optional, ClassVar
|
5 |
import hashlib
|
6 |
|
7 |
|
8 |
class Player(BaseModel):
|
9 |
+
teams: ClassVar[list[str]] = ['Fraser Valley United', 'Everglade FC', 'Yucatan Force', 'Tierra Alta FC']
|
10 |
number: int
|
11 |
name: str
|
12 |
age: int
|
|
|
88 |
return cls.model_validate(data)
|
89 |
|
90 |
@classmethod
|
91 |
+
def get_players(cls, team=None):
|
92 |
for filename in os.listdir("/workspace/data/huge-league/players"):
|
93 |
+
player = cls.load(filename)
|
94 |
+
if team and player.team != team:
|
95 |
+
continue
|
96 |
+
yield player
|
97 |
|
98 |
def save_image(self, image_bytes):
|
99 |
filename = self.filename.replace(".json", ".png")
|
data/huge-league/games/semifinal_1.json
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"description": "Ram\u00f3n Jim\u00e9nez scored with a powerful left-footed strike from the edge of the box.",
|
4 |
+
"event": "Goal",
|
5 |
+
"minute": "12",
|
6 |
+
"player": "Ram\u00f3n Jim\u00e9nez #10",
|
7 |
+
"team": "Yucatan Force"
|
8 |
+
},
|
9 |
+
{
|
10 |
+
"description": "Ricardo Cordero received a yellow card for a late tackle on Jos\u00e9 Delgado.",
|
11 |
+
"event": "Yellow Card",
|
12 |
+
"minute": "28",
|
13 |
+
"player": "Ricardo Cordero #5",
|
14 |
+
"team": "Tierra Alta FC"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"description": "Felipe Z\u00fa\u00f1iga equalized with a header from a cross by Gabriel Fonseca.",
|
18 |
+
"event": "Goal",
|
19 |
+
"minute": "34",
|
20 |
+
"player": "Felipe Z\u00fa\u00f1iga #10",
|
21 |
+
"team": "Tierra Alta FC"
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"description": "Javier Mart\u00ednez replaced Ram\u00f3n S\u00e1nchez to add more width to the attack.",
|
25 |
+
"event": "Substitution",
|
26 |
+
"minute": "45+1",
|
27 |
+
"player": "Javier Mart\u00ednez #20",
|
28 |
+
"team": "Yucatan Force"
|
29 |
+
},
|
30 |
+
{
|
31 |
+
"description": "Jos\u00e9 Delgado curled in a stunning right-foot shot from outside the box.",
|
32 |
+
"event": "Goal",
|
33 |
+
"minute": "54",
|
34 |
+
"player": "Jos\u00e9 Delgado #11",
|
35 |
+
"team": "Yucatan Force"
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"description": "Leonardo Salazar came on for Esteban Camacho to inject fresh energy into the attack.",
|
39 |
+
"event": "Substitution",
|
40 |
+
"minute": "67",
|
41 |
+
"player": "Leonardo Salazar #18",
|
42 |
+
"team": "Tierra Alta FC"
|
43 |
+
},
|
44 |
+
{
|
45 |
+
"description": "Santiago Ortega was booked for a tactical foul on Joaqu\u00edn Araya.",
|
46 |
+
"event": "Yellow Card",
|
47 |
+
"minute": "70",
|
48 |
+
"player": "Santiago Ortega #7",
|
49 |
+
"team": "Yucatan Force"
|
50 |
+
},
|
51 |
+
{
|
52 |
+
"description": "Sebasti\u00e1n Alvarado replaced Mauricio P\u00e9rez to bolster the midfield.",
|
53 |
+
"event": "Substitution",
|
54 |
+
"minute": "79",
|
55 |
+
"player": "Sebasti\u00e1n Alvarado #20",
|
56 |
+
"team": "Tierra Alta FC"
|
57 |
+
},
|
58 |
+
{
|
59 |
+
"description": "Emilio Navarro came on for H\u00e9ctor L\u00f3pez to help secure the lead.",
|
60 |
+
"event": "Substitution",
|
61 |
+
"minute": "85",
|
62 |
+
"player": "Emilio Navarro #18",
|
63 |
+
"team": "Yucatan Force"
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"description": "Pablo Ure\u00f1a received a caution for dissent after a disputed throw-in call.",
|
67 |
+
"event": "Yellow Card",
|
68 |
+
"minute": "90+3",
|
69 |
+
"player": "Pablo Ure\u00f1a #8",
|
70 |
+
"team": "Tierra Alta FC"
|
71 |
+
}
|
72 |
+
]
|
data/huge-league/games/semifinal_2.json
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"description": "Lucas Harris cuts in from the right wing and curls a shot into the far corner.",
|
4 |
+
"event": "Goal",
|
5 |
+
"minute": "5",
|
6 |
+
"player": "Lucas Harris 11",
|
7 |
+
"team": "Fraser Valley United"
|
8 |
+
},
|
9 |
+
{
|
10 |
+
"description": "Brandon Hernandez receives a yellow card for a late tackle on Mason Walker.",
|
11 |
+
"event": "Yellow Card",
|
12 |
+
"minute": "16",
|
13 |
+
"player": "Brandon Hernandez 5",
|
14 |
+
"team": "Everglade FC"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"description": "Ryan Williams equalizes with a powerful header from a corner delivered by Michael Smith.",
|
18 |
+
"event": "Goal",
|
19 |
+
"minute": "28",
|
20 |
+
"player": "Ryan Williams 7",
|
21 |
+
"team": "Everglade FC"
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"description": "Elijah Gagnon comes on for Thomas Walker to add fresh legs up front.",
|
25 |
+
"event": "Substitution",
|
26 |
+
"minute": "33",
|
27 |
+
"player": "Elijah Gagnon 19",
|
28 |
+
"team": "Fraser Valley United"
|
29 |
+
},
|
30 |
+
{
|
31 |
+
"description": "Andrew Anderson gives Everglade FC the lead with a finesse shot from the left side.",
|
32 |
+
"event": "Goal",
|
33 |
+
"minute": "41",
|
34 |
+
"player": "Andrew Anderson 9",
|
35 |
+
"team": "Everglade FC"
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"description": "Jacob Thompson is booked for dissent following a controversial foul call.",
|
39 |
+
"event": "Yellow Card",
|
40 |
+
"minute": "45+1",
|
41 |
+
"player": "Jacob Thompson 4",
|
42 |
+
"team": "Fraser Valley United"
|
43 |
+
},
|
44 |
+
{
|
45 |
+
"description": "Thomas Brown levels the score with a spectacular long-range strike.",
|
46 |
+
"event": "Goal",
|
47 |
+
"minute": "56",
|
48 |
+
"player": "Thomas Brown 7",
|
49 |
+
"team": "Fraser Valley United"
|
50 |
+
},
|
51 |
+
{
|
52 |
+
"description": "Ryan Brown replaces Michael Smith to add energy on the right wing.",
|
53 |
+
"event": "Substitution",
|
54 |
+
"minute": "63",
|
55 |
+
"player": "Ryan Brown 21",
|
56 |
+
"team": "Everglade FC"
|
57 |
+
},
|
58 |
+
{
|
59 |
+
"description": "Owen Campbell is brought on for Mason Walker to bolster the midfield.",
|
60 |
+
"event": "Substitution",
|
61 |
+
"minute": "74",
|
62 |
+
"player": "Owen Campbell 17",
|
63 |
+
"team": "Fraser Valley United"
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"description": "Elijah Gagnon puts Fraser Valley United ahead with a close-range finish.",
|
67 |
+
"event": "Goal",
|
68 |
+
"minute": "80",
|
69 |
+
"player": "Elijah Gagnon 19",
|
70 |
+
"team": "Fraser Valley United"
|
71 |
+
},
|
72 |
+
{
|
73 |
+
"description": "Matthew Martin equalizes in dramatic fashion with a header from a free-kick.",
|
74 |
+
"event": "Goal",
|
75 |
+
"minute": "87",
|
76 |
+
"player": "Matthew Martin 10",
|
77 |
+
"team": "Everglade FC"
|
78 |
+
},
|
79 |
+
{
|
80 |
+
"description": "Joseph Thomas enters for Austin Jackson to strengthen the team's defense.",
|
81 |
+
"event": "Substitution",
|
82 |
+
"minute": "90+3",
|
83 |
+
"player": "Joseph Thomas 15",
|
84 |
+
"team": "Everglade FC"
|
85 |
+
},
|
86 |
+
{
|
87 |
+
"description": "Ryan Williams receives a yellow card for time-wasting.",
|
88 |
+
"event": "Yellow Card",
|
89 |
+
"minute": "90+5",
|
90 |
+
"player": "Ryan Williams 7",
|
91 |
+
"team": "Everglade FC"
|
92 |
+
},
|
93 |
+
{
|
94 |
+
"description": "Everglade FC wins the penalty shootout 4-2, securing a spot in the final.",
|
95 |
+
"event": "Penalty Shootout",
|
96 |
+
"minute": "Full-Time",
|
97 |
+
"player": "Various",
|
98 |
+
"team": "Everglade FC"
|
99 |
+
}
|
100 |
+
]
|