import re
import pandas as pd
import gradio as gr
# ====================== CONFIG ======================
CSV_FILE = "game_data.csv"
df = pd.read_csv(CSV_FILE)
TOTAL_STAGES = len(df)
# ====================== HELPER ======================
def extract_start_seconds(url_str: str, default_seconds: int = 78) -> int:
"""
Try to extract start time from:
- t=78
- start=78
If not found, fall back to default_seconds (78).
Supports URLs like: ...&t=78 or ...?t=78
"""
if not url_str:
return default_seconds
s = str(url_str)
# Common patterns: t=78 or start=78 (allow ?t=78 or &t=78)
m = re.search(r"(?:[?&](?:t|start))=(\d+)", s)
if m:
return int(m.group(1))
return default_seconds
def get_embed_url(youtube_url: str, start_seconds: int) -> str:
"""
Converts a standard YouTube watch/share link into an embed link.
Adds start time using ?start=...
"""
url_str = str(youtube_url).strip()
if not url_str:
return ""
video_id = ""
if "youtu.be/" in url_str:
video_id = url_str.split("youtu.be/")[1].split("?")[0].split("&")[0]
elif "v=" in url_str:
video_id = url_str.split("v=")[1].split("&")[0].split("?")[0]
elif "embed/" in url_str:
# If it's already an embed link, keep it but normalize start
# (best effort: do not try to re-parse ID)
base = url_str.split("?")[0]
return f"{base}?start={start_seconds}"
else:
return ""
return f"https://www.youtube.com/embed/{video_id}?start={start_seconds}"
def generate_iframe_html(embed_url: str) -> str:
"""Wraps an embed link inside a responsive HTML iframe."""
if not embed_url:
return "
⚠️ Invalid YouTube link found in CSV!
"
return f"""
"""
def get_stage_data(stage_index: int):
if stage_index < 0 or stage_index >= TOTAL_STAGES:
return None
row = df.iloc[stage_index]
raw_url = row.get("youtube_link", "")
start_seconds = extract_start_seconds(raw_url, default_seconds=78)
embed_url = get_embed_url(raw_url, start_seconds=start_seconds)
iframe_code = generate_iframe_html(embed_url)
title = row.get("title", f"Stage {stage_index + 1}")
return {
"stage_no": row.get("stage_no", stage_index + 1),
"title": title,
"song_name": str(row["song_name"]).strip(),
"iframe_html": iframe_code,
"lyrics": row.get("lyrics", "No lyrics available."),
}
# ====================== GAME LOGIC ======================
def start_game():
"""
Initializes stage but hides the YouTube iframe until user clicks Play Clip.
"""
stage = get_stage_data(0)
if stage is None:
return (
0, # current_stage_idx
0, # current_score
"", # video_html_ui (empty -> hidden)
f"", # stage_title_ui
"", # lyrics_ui (content)
"", # guess_input value
"", # feedback_ui
gr.update(visible=False), # video_box visible
gr.update(visible=True), # game_box visible
gr.update(visible=False), # game_over_box visible
gr.update(visible=True), # play_btn visible
gr.update(visible=False), # next_btn visible
)
return (
0,
0,
"", # hide video until play
f"## 🎵 {stage['title']}",
stage["lyrics"],
"",
"",
gr.update(visible=False), # video_box hidden
gr.update(visible=True), # game_box visible
gr.update(visible=False), # game_over_box hidden
gr.update(visible=True), # show Play button
gr.update(visible=False), # next hidden
)
def play_clip(current_idx: int):
"""
Generates and shows the iframe for the current stage.
"""
stage = get_stage_data(current_idx)
if not stage:
return (
"", # video_html_ui
gr.update(visible=False), # video_box
)
return (
stage["iframe_html"],
gr.update(visible=True), # show video
)
def show_lyrics():
return gr.update(visible=True)
def check_guess(guess, current_idx, score):
stage = get_stage_data(current_idx)
if not stage:
return (
score,
"Error: invalid stage.",
gr.update(visible=False), # next_btn
gr.update(visible=False), # game_over_box
)
is_correct = guess.strip().lower() == stage["song_name"].lower()
if is_correct:
score += 1
feedback = "🎉 **Correct!**"
else:
feedback = f"❌ **Incorrect.** Correct answer: **{stage['song_name']}**"
next_idx = current_idx + 1
is_last = next_idx >= TOTAL_STAGES
return (
score,
feedback,
gr.update(visible=not is_last), # show next if not last
gr.update(visible=is_last), # show game over if last
)
def next_stage(current_idx):
"""
Advances to next stage, hides iframe again, and shows Play button.
"""
next_idx = current_idx + 1
stage = get_stage_data(next_idx)
if not stage:
# End: keep iframe hidden, hide next
return (
current_idx,
"", # video_html_ui cleared
f"",
"", # lyrics_ui content
"",
"",
gr.update(visible=False), # video_box hidden
gr.update(visible=False), # next_btn hidden
)
return (
next_idx,
"", # clear video_html_ui
f"## 🎵 {stage['title']}",
stage["lyrics"],
"",
"",
gr.update(visible=False), # hide video again
gr.update(visible=False), # next_btn hidden until correct
)
def end_game_summary(score):
return f"## 🏆 Game Over!\n\nYou scored **{score}/{TOTAL_STAGES}**!"
# ====================== UI ======================
with gr.Blocks() as demo:
gr.Markdown("# 🎧 Ultimate YouTube Music Guessing Game")
gr.Markdown("Click ▶️ Play Clip to start. Then guess the song name!")
current_stage_idx = gr.State(0)
current_score = gr.State(0)
with gr.Column(visible=True) as game_box:
stage_title_ui = gr.Markdown()
with gr.Row():
with gr.Column():
# Video area (we toggle visibility)
with gr.Column(visible=False) as video_box:
video_html_ui = gr.HTML()
play_btn = gr.Button("▶️ Play Clip", variant="secondary")
lyrics_btn = gr.Button("💡 Show Lyrics")
lyrics_ui = gr.Markdown(visible=False)
with gr.Column():
score_display = gr.Markdown("### Score: 0")
guess_input = gr.Textbox(label="Your Guess", placeholder="Enter song name...")
submit_btn = gr.Button("Submit", variant="primary")
feedback_ui = gr.Markdown()
next_btn = gr.Button("Next Stage ➡️", visible=False)
with gr.Column(visible=False) as game_over_box:
summary_ui = gr.Markdown()
restart_btn = gr.Button("Play Again 🔄")
# Start/load
demo.load(
fn=start_game,
outputs=[
current_stage_idx,
current_score,
video_html_ui, # hidden (empty)
stage_title_ui,
lyrics_ui, # lyrics content
guess_input,
feedback_ui,
video_box, # hide video
game_box, # show/hide game panel
game_over_box, # show/hide over panel
play_btn, # show play button
next_btn, # next hidden
],
)
# Play button (show iframe)
play_btn.click(
fn=play_clip,
inputs=[current_stage_idx],
outputs=[video_html_ui, video_box],
)
# Lyrics visibility
lyrics_btn.click(
fn=show_lyrics,
outputs=lyrics_ui
)
# Submit guess
submit_btn.click(
fn=check_guess,
inputs=[guess_input, current_stage_idx, current_score],
outputs=[current_score, feedback_ui, next_btn, game_over_box],
).then(
fn=lambda s: f"### Score: {s}",
inputs=current_score,
outputs=score_display
).then(
fn=end_game_summary,
inputs=current_score,
outputs=summary_ui
)
# Next stage
next_btn.click(
fn=next_stage,
inputs=[current_stage_idx],
outputs=[
current_stage_idx, # stage idx
video_html_ui, # clear video html
stage_title_ui, # update title
lyrics_ui, # update lyrics content (still hidden unless user clicks)
guess_input, # clear guess
feedback_ui, # clear feedback
video_box, # hide video again
next_btn, # keep next hidden until correct guess
],
).then(
fn=lambda: gr.update(visible=True), # show play btn again
inputs=[],
outputs=[play_btn]
)
# Restart
restart_btn.click(
fn=start_game,
outputs=[
current_stage_idx,
current_score,
video_html_ui,
stage_title_ui,
lyrics_ui,
guess_input,
feedback_ui,
video_box,
game_box,
game_over_box,
play_btn,
next_btn,
],
).then(
fn=lambda s: f"### Score: {s}",
inputs=current_score,
outputs=score_display
)
demo.launch()