""" 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()