File size: 10,103 Bytes
7e412f4
796ef1c
 
 
 
 
 
 
 
 
 
7e412f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796ef1c
 
7e412f4
796ef1c
 
7e412f4
 
 
796ef1c
 
 
7e412f4
796ef1c
7e412f4
796ef1c
7e412f4
 
 
 
 
796ef1c
7e412f4
796ef1c
7e412f4
796ef1c
 
 
 
 
 
 
 
 
7e412f4
 
 
 
 
 
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
 
 
796ef1c
 
 
 
 
7e412f4
 
796ef1c
 
 
7e412f4
 
 
 
 
796ef1c
 
 
 
 
7e412f4
796ef1c
 
 
 
7e412f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
 
796ef1c
 
 
 
7e412f4
 
 
796ef1c
 
 
 
7e412f4
796ef1c
 
7e412f4
 
 
796ef1c
 
7e412f4
796ef1c
 
 
 
 
7e412f4
796ef1c
 
 
 
7e412f4
 
796ef1c
 
 
 
 
 
 
 
 
 
7e412f4
796ef1c
 
 
 
 
 
7e412f4
796ef1c
 
7e412f4
 
 
 
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
796ef1c
 
 
7e412f4
 
 
 
 
 
 
 
 
 
 
 
796ef1c
 
 
7e412f4
 
 
 
 
 
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
 
 
 
 
 
 
 
796ef1c
7e412f4
 
 
 
796ef1c
 
 
 
 
 
 
 
 
 
 
 
 
7e412f4
796ef1c
 
7e412f4
796ef1c
 
 
 
 
 
 
 
3dc741f
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
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 "<p style='color:red;'>⚠️ Invalid YouTube link found in CSV!</p>"

    return f"""
    <div style="display: flex; justify-content: center; margin-bottom: 15px;">
        <iframe
            width="100%"
            height="315"
            src="{embed_url}"
            title="YouTube video player"
            frameborder="0"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
            allowfullscreen>
        </iframe>
    </div>
    """


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