Download app.py from ayoubsa/WC: direct link, hf CLI and curl.
- Browser
- Download file 18.1 kB
-
https://huggingface.co/spaces/ayoubsa/WC/resolve/main/app.py
- Command line
-
hf download hf://spaces/ayoubsa/WC/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ayoubsa/WC/resolve/main/app.py
18.1 kB
| """ | |
| World Cup Storyteller — Hugging Face Spaces app.py | |
| NLP Homework 4 — ARI 525 | |
| Upload this file + requirements.txt + your 3 CSVs to your HF Space. | |
| Set GROQ_API_KEY as a Space secret in Settings. | |
| """ | |
| import os | |
| import time | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| import pandas as pd | |
| import numpy as np | |
| import gradio as gr | |
| from groq import Groq | |
| from sentence_transformers import SentenceTransformer | |
| import faiss | |
| # ----------------------------------------------- | |
| # 1. CONFIG | |
| # ----------------------------------------------- | |
| # Loaded from HF Space secret (never hardcode this) | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") | |
| MODEL = "llama-3.1-8b-instant" | |
| client = Groq(api_key=GROQ_API_KEY) | |
| # ----------------------------------------------- | |
| # 2. LOAD DATA | |
| # CSVs must be uploaded to your HF Space root folder | |
| # ----------------------------------------------- | |
| DATA_PATH = "./WC_data/" # Same folder as app.py on HF Space | |
| try: | |
| cups = pd.read_csv(DATA_PATH + "WorldCups.csv") | |
| matches = pd.read_csv(DATA_PATH + "WorldCupMatches.csv") | |
| players = pd.read_csv(DATA_PATH + "WorldCupPlayers.csv") | |
| print("✅ Data loaded successfully.") | |
| except FileNotFoundError as e: | |
| raise RuntimeError( | |
| "CSV files not found. Make sure WorldCups.csv, WorldCupMatches.csv, " | |
| "and WorldCupPlayers.csv are uploaded to your HF Space root folder." | |
| ) from e | |
| # ----------------------------------------------- | |
| # 3. DATA CLEANING | |
| # ----------------------------------------------- | |
| def clean_data(cups, matches): | |
| matches_clean = matches.dropna( | |
| subset=["Home Team Name", "Away Team Name", "Home Team Goals", "Away Team Goals"] | |
| ).copy() | |
| matches_clean["Home Team Name"] = matches_clean["Home Team Name"].str.strip() | |
| matches_clean["Away Team Name"] = matches_clean["Away Team Name"].str.strip() | |
| matches_clean["Home Team Goals"] = matches_clean["Home Team Goals"].astype(int) | |
| matches_clean["Away Team Goals"] = matches_clean["Away Team Goals"].astype(int) | |
| cups_clean = cups.dropna(subset=["Year", "Winner"]).copy() | |
| cups_clean["Year"] = cups_clean["Year"].astype(int) | |
| return cups_clean, matches_clean | |
| cups_clean, matches_clean = clean_data(cups, matches) | |
| available_years = sorted(cups_clean["Year"].unique().tolist()) | |
| available_teams = sorted(set( | |
| matches_clean["Home Team Name"].tolist() + | |
| matches_clean["Away Team Name"].tolist() | |
| )) | |
| # ----------------------------------------------- | |
| # 4. CONTEXT BUILDERS | |
| # ----------------------------------------------- | |
| def build_team_context(team, year, cups, matches): | |
| cup_info = cups[cups["Year"] == year] | |
| if cup_info.empty: | |
| return None, f"No data found for year {year}." | |
| cup = cup_info.iloc[0] | |
| team_matches = matches[ | |
| (matches["Year"] == year) & | |
| ((matches["Home Team Name"] == team) | (matches["Away Team Name"] == team)) | |
| ].sort_values("Stage") | |
| if team_matches.empty: | |
| return None, f"{team} did not participate in the {year} World Cup." | |
| match_lines = [] | |
| for _, row in team_matches.iterrows(): | |
| home, away = row["Home Team Name"], row["Away Team Name"] | |
| hg, ag = int(row["Home Team Goals"]), int(row["Away Team Goals"]) | |
| if home == team: | |
| result = "WIN" if hg > ag else ("DRAW" if hg == ag else "LOSS") | |
| line = f"[{row['Stage']}] {team} vs {away}: {hg}-{ag} ({result})" | |
| else: | |
| result = "WIN" if ag > hg else ("DRAW" if hg == ag else "LOSS") | |
| line = f"[{row['Stage']}] {team} vs {home}: {ag}-{hg} ({result})" | |
| match_lines.append(line) | |
| final_note = "" | |
| if cup.get("Winner") == team: | |
| final_note = f"{team} WON the {year} World Cup! 🏆" | |
| elif cup.get("Runners-Up") == team: | |
| final_note = f"{team} were runners-up in {year}." | |
| elif cup.get("Third") == team: | |
| final_note = f"{team} finished third in {year}." | |
| context = ( | |
| f"{team}'s Journey — World Cup {year}\n" | |
| f"Host: {cup.get('Country', 'Unknown')}\n" | |
| f"{final_note}\n\n" | |
| f"Match-by-match results:\n" + | |
| "\n".join(match_lines) | |
| ) | |
| return context, None | |
| def build_edition_context(year, cups, matches): | |
| cup_info = cups[cups["Year"] == year] | |
| if cup_info.empty: | |
| return None, f"No data found for year {year}." | |
| cup = cup_info.iloc[0] | |
| edition_matches = matches[matches["Year"] == year].sort_values("Stage") | |
| match_lines = [ | |
| f"[{row['Stage']}] {row['Home Team Name']} {int(row['Home Team Goals'])} " | |
| f"- {int(row['Away Team Goals'])} {row['Away Team Name']}" | |
| for _, row in edition_matches.iterrows() | |
| ] | |
| context = ( | |
| f"World Cup {year} — Host: {cup.get('Country', 'Unknown')}\n" | |
| f"Winner: {cup.get('Winner', 'Unknown')}\n" | |
| f"Runners-up: {cup.get('Runners-Up', 'Unknown')}\n" | |
| f"Third Place: {cup.get('Third', 'Unknown')}\n" | |
| f"Goals Scored: {cup.get('GoalsScored', 'Unknown')}\n" | |
| f"Teams: {cup.get('QualifiedTeams', 'Unknown')}\n" | |
| f"Attendance: {cup.get('Attendance', 'Unknown')}\n\n" | |
| f"All Matches:\n" + | |
| "\n".join(match_lines) | |
| ) | |
| return context, None | |
| # ----------------------------------------------- | |
| # 5. PROMPTS | |
| # ----------------------------------------------- | |
| SYSTEM_PROMPT = """ | |
| You are a passionate, knowledgeable sports journalist and storyteller specializing in | |
| FIFA World Cup history. Your job is to turn raw match data into vivid, engaging, | |
| narrative-driven stories about World Cup tournaments and team journeys. | |
| Guidelines: | |
| - Write in a natural, flowing narrative style (not bullet points) | |
| - Make the story feel alive — build tension, highlight dramatic moments | |
| - Use the match data accurately — never invent scores or results | |
| - Adapt your tone to the user's request (documentary, dramatic, casual, etc.) | |
| - Keep the story between 250-400 words unless asked otherwise | |
| """.strip() | |
| RAG_SYSTEM_PROMPT = """ | |
| You are a passionate sports journalist specializing in FIFA World Cup history. | |
| You will be given retrieved historical World Cup data as context, followed by | |
| a user request. Use the retrieved data to write an accurate, vivid, engaging | |
| narrative story. Only use information present in the retrieved context. | |
| Guidelines: | |
| - Write in flowing narrative prose (not bullet points) | |
| - Build tension and highlight drama | |
| - Stick strictly to the facts in the retrieved data | |
| - Adapt tone to user's request | |
| - Keep the story between 250-400 words unless asked otherwise | |
| """.strip() | |
| FEW_SHOT_EXAMPLES = [ | |
| { | |
| "context": ( | |
| "France's Journey — World Cup 1998\nHost: France\n" | |
| "France WON the 1998 World Cup! 🏆\n\nMatch-by-match results:\n" | |
| "[Group Stage] France vs South Africa: 3-0 (WIN)\n" | |
| "[Group Stage] France vs Saudi Arabia: 4-0 (WIN)\n" | |
| "[Group Stage] France vs Denmark: 2-1 (WIN)\n" | |
| "[Round of 16] France vs Paraguay: 1-0 (WIN)\n" | |
| "[Quarter-finals] France vs Italy: 0-0 (WIN via penalties)\n" | |
| "[Semi-finals] France vs Croatia: 2-1 (WIN)\n" | |
| "[Final] France vs Brazil: 3-0 (WIN)" | |
| ), | |
| "story": ( | |
| "It was the summer that France found its destiny on home soil. Les Bleus entered " | |
| "the 1998 World Cup as hosts with immense pressure, but from the very first " | |
| "whistle, they played with a quiet, relentless authority.\n\n" | |
| "The group stage was a statement — South Africa swept aside 3-0, Saudi Arabia " | |
| "dismantled 4-0, and Denmark edged out 2-1. By the knockout rounds, France " | |
| "carried the weight of a nation's expectations into every match.\n\n" | |
| "Paraguay made them suffer — a lone goal in extra time was all that separated " | |
| "the sides. Italy pushed them to penalties, a nerve-shredding duel that France " | |
| "survived with ice-cold nerves. Then came Croatia — France trailed before turning " | |
| "the game on its head to win 2-1.\n\n" | |
| "The final against defending champions Brazil became a coronation. Three goals, " | |
| "zero reply. The Stade de France erupted, and a generation of French children " | |
| "discovered what it felt like to be champions of the world." | |
| ) | |
| }, | |
| { | |
| "context": ( | |
| "West Germany's Journey — World Cup 1954\nHost: Switzerland\n" | |
| "West Germany WON the 1954 World Cup! 🏆\n\nMatch-by-match results:\n" | |
| "[Group Stage] West Germany vs Turkey: 4-1 (WIN)\n" | |
| "[Group Stage] West Germany vs Hungary: 3-8 (LOSS)\n" | |
| "[Group Stage Playoff] West Germany vs Turkey: 7-2 (WIN)\n" | |
| "[Quarter-finals] West Germany vs Yugoslavia: 2-0 (WIN)\n" | |
| "[Semi-finals] West Germany vs Austria: 6-1 (WIN)\n" | |
| "[Final] West Germany vs Hungary: 3-2 (WIN)" | |
| ), | |
| "story": ( | |
| "They called it the Miracle of Bern, and for good reason. No one believed West " | |
| "Germany could win the 1954 World Cup — least of all after Hungary handed them " | |
| "an 8-3 humiliation in the group stage.\n\n" | |
| "But West Germany, crafty and resilient, rested key players for that match and " | |
| "quietly plotted their path to the final. They dispatched Turkey twice, squeezed " | |
| "past Yugoslavia, then demolished Austria 6-1 in a dazzling semi-final.\n\n" | |
| "The final was a rematch nobody expected. Hungary — the Mighty Magyars, unbeaten " | |
| "for four years — led 2-0 within eight minutes. The world assumed it was over.\n\n" | |
| "It was not. West Germany clawed back to 2-2, and then, six minutes from the end, " | |
| "Helmut Rahn struck. 3-2. A country still rebuilding from the rubble of war had " | |
| "become world champions." | |
| ) | |
| } | |
| ] | |
| # ----------------------------------------------- | |
| # 6. GENERATION FUNCTIONS | |
| # ----------------------------------------------- | |
| def generate_zeroshot(user_prompt, context): | |
| msg = f"Here is the World Cup data:\n---\n{context}\n---\n\nUser request: {user_prompt}" | |
| start = time.time() | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": msg} | |
| ], | |
| temperature=0.8, | |
| max_tokens=600 | |
| ) | |
| elapsed = round(time.time() - start, 2) | |
| return response.choices[0].message.content.strip(), elapsed | |
| def generate_fewshot(user_prompt, context): | |
| messages = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| for ex in FEW_SHOT_EXAMPLES: | |
| messages.append({ | |
| "role": "user", | |
| "content": f"Here is the World Cup data:\n---\n{ex['context']}\n---\n\nUser request: Tell the story of this team's World Cup journey." | |
| }) | |
| messages.append({"role": "assistant", "content": ex["story"]}) | |
| messages.append({ | |
| "role": "user", | |
| "content": f"Here is the World Cup data:\n---\n{context}\n---\n\nUser request: {user_prompt}" | |
| }) | |
| start = time.time() | |
| response = client.chat.completions.create( | |
| model=MODEL, messages=messages, temperature=0.8, max_tokens=600 | |
| ) | |
| elapsed = round(time.time() - start, 2) | |
| return response.choices[0].message.content.strip(), elapsed | |
| # RAG setup | |
| print("⏳ Loading embedding model...") | |
| embedder = SentenceTransformer("all-MiniLM-L6-v2") | |
| def build_knowledge_base(cups, matches): | |
| chunks, metadata = [], [] | |
| for _, cup_row in cups.iterrows(): | |
| year = int(cup_row["Year"]) | |
| year_matches = matches[matches["Year"] == year] | |
| teams = set( | |
| year_matches["Home Team Name"].tolist() + | |
| year_matches["Away Team Name"].tolist() | |
| ) | |
| for team in teams: | |
| ctx, err = build_team_context(team, year, cups, matches) | |
| if ctx: | |
| chunks.append(ctx) | |
| metadata.append({"team": team, "year": year}) | |
| ctx, err = build_edition_context(year, cups, matches) | |
| if ctx: | |
| chunks.append(ctx) | |
| metadata.append({"team": "ALL", "year": year}) | |
| return chunks, metadata | |
| print("⏳ Building knowledge base...") | |
| kb_chunks, kb_metadata = build_knowledge_base(cups_clean, matches_clean) | |
| print("⏳ Embedding knowledge base...") | |
| kb_embeddings = embedder.encode(kb_chunks, show_progress_bar=False, convert_to_numpy=True) | |
| dim = kb_embeddings.shape[1] | |
| faiss_index = faiss.IndexFlatL2(dim) | |
| faiss_index.add(kb_embeddings) | |
| print(f"✅ RAG index ready — {faiss_index.ntotal} vectors") | |
| def retrieve_context(query, top_k=3): | |
| query_vec = embedder.encode([query], convert_to_numpy=True) | |
| distances, indices = faiss_index.search(query_vec, top_k) | |
| retrieved = [] | |
| for idx in indices[0]: | |
| if idx < len(kb_chunks): | |
| meta = kb_metadata[idx] | |
| retrieved.append(f"[Retrieved: {meta['team']} — {meta['year']}]\n{kb_chunks[idx]}") | |
| return "\n\n---\n\n".join(retrieved) | |
| def generate_rag(user_prompt): | |
| retrieved = retrieve_context(user_prompt, top_k=3) | |
| msg = f"Retrieved World Cup data:\n---\n{retrieved}\n---\n\nUser request: {user_prompt}" | |
| start = time.time() | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": RAG_SYSTEM_PROMPT}, | |
| {"role": "user", "content": msg} | |
| ], | |
| temperature=0.8, | |
| max_tokens=600 | |
| ) | |
| elapsed = round(time.time() - start, 2) | |
| return response.choices[0].message.content.strip(), elapsed | |
| # ----------------------------------------------- | |
| # 7. GRADIO UI | |
| # ----------------------------------------------- | |
| def generate_story_ui(mode, year, team, user_prompt, approach): | |
| year = int(year) | |
| if mode == "Team Journey": | |
| if not team: | |
| return "⚠️ Please select a team.", "" | |
| context, err = build_team_context(team, year, cups_clean, matches_clean) | |
| else: | |
| context, err = build_edition_context(year, cups_clean, matches_clean) | |
| if err: | |
| return f"⚠️ {err}", "" | |
| if not user_prompt.strip(): | |
| if mode == "Team Journey": | |
| user_prompt = f"Tell me {team}'s {year} World Cup story in a dramatic, engaging way." | |
| else: | |
| user_prompt = f"Tell the full story of the {year} World Cup — the drama, the upsets, the champion." | |
| try: | |
| if approach == "Zero-Shot": | |
| story, elapsed = generate_zeroshot(user_prompt, context) | |
| info = f"⚡ Zero-Shot | ⏱️ {elapsed}s | 📝 {len(story.split())} words" | |
| elif approach == "Few-Shot": | |
| story, elapsed = generate_fewshot(user_prompt, context) | |
| info = f"📖 Few-Shot | ⏱️ {elapsed}s | 📝 {len(story.split())} words" | |
| else: | |
| story, elapsed = generate_rag(user_prompt) | |
| info = f"🔍 RAG | ⏱️ {elapsed}s | 📝 {len(story.split())} words" | |
| return story, info | |
| except Exception as e: | |
| return f"❌ Error: {str(e)}", "" | |
| year_choices = [str(y) for y in available_years] | |
| with gr.Blocks( | |
| title="⚽ World Cup Storyteller", | |
| theme=gr.themes.Base(), | |
| css=""" | |
| #header { text-align: center; padding: 1.5em 0 0.5em 0; } | |
| #header h1 { font-size: 2.2em; margin-bottom: 0.1em; } | |
| #header p { color: #888; font-size: 1.05em; } | |
| #story-box textarea { font-size: 1.05em; line-height: 1.8; } | |
| #info-bar { font-size: 0.9em; color: #555; margin-top: 0.3em; } | |
| .approach-note { font-size: 0.85em; color: #777; margin-top: 0.4em; } | |
| """ | |
| ) as demo: | |
| with gr.Column(elem_id="header"): | |
| gr.Markdown("# ⚽ World Cup Storyteller") | |
| gr.Markdown("Generate vivid, narrative-driven stories about any World Cup edition or team journey.") | |
| with gr.Row(): | |
| # --- Left panel: controls --- | |
| with gr.Column(scale=1, min_width=280): | |
| gr.Markdown("### ⚙️ Settings") | |
| mode = gr.Radio( | |
| choices=["Team Journey", "Full Edition"], | |
| value="Team Journey", | |
| label="Storytelling Mode" | |
| ) | |
| year = gr.Dropdown( | |
| choices=year_choices, | |
| value="2002", | |
| label="World Cup Year" | |
| ) | |
| team = gr.Dropdown( | |
| choices=available_teams, | |
| value="Brazil", | |
| label="Team (Team Journey only)" | |
| ) | |
| approach = gr.Radio( | |
| choices=["Zero-Shot", "Few-Shot", "RAG"], | |
| value="Few-Shot", | |
| label="NLP Approach" | |
| ) | |
| gr.Markdown( | |
| "- **Zero-Shot** — No examples, direct generation\n" | |
| "- **Few-Shot** — Guided by hand-crafted story examples\n" | |
| "- **RAG** — Retrieves context from full knowledge base", | |
| elem_classes="approach-note" | |
| ) | |
| # --- Right panel: prompt + output --- | |
| with gr.Column(scale=2): | |
| gr.Markdown("### ✍️ Your Prompt") | |
| user_prompt = gr.Textbox( | |
| placeholder='e.g. "Tell Brazil\'s 2002 story like a sports documentary" — or leave blank for a default story.', | |
| label="Free-form prompt (optional)", | |
| lines=3 | |
| ) | |
| btn = gr.Button("🎙️ Generate Story", variant="primary", size="lg") | |
| gr.Markdown("### 📖 Story") | |
| story_out = gr.Textbox( | |
| label="", | |
| lines=16, | |
| interactive=False, | |
| elem_id="story-box" | |
| ) | |
| info_out = gr.Markdown("", elem_id="info-bar") | |
| btn.click( | |
| fn=generate_story_ui, | |
| inputs=[mode, year, team, user_prompt, approach], | |
| outputs=[story_out, info_out] | |
| ) | |
| gr.Markdown( | |
| "---\n*Data: FIFA World Cup dataset (1930–2014) · Model: Llama 3 8B via Groq · " | |
| "Embeddings: all-MiniLM-L6-v2 · Built for NLP HW4 — ARI 525*" | |
| ) | |
| demo.launch() | |