import gradio as gr import os import random from pathlib import Path # Build a mapping of audio clips to their corresponding images def build_clip_mapping(audio_dir): """Map each audio file to its corresponding image based on matching base names""" audio_files = sorted([f for f in os.listdir(audio_dir) if f.endswith('.mp3')]) image_files = sorted([f for f in os.listdir(audio_dir) if f.endswith('.png')]) print(f"Found {len(audio_files)} audio files in {audio_dir}") print(f"Found {len(image_files)} image files in {audio_dir}") mapping = {} for audio_file in audio_files: audio_base = os.path.splitext(audio_file)[0] for image_file in image_files: image_base = os.path.splitext(image_file)[0] if audio_base == image_base: mapping[audio_file] = image_file break print(f"Successfully mapped {len(mapping)} audio-image pairs") return mapping class QuizApp: def __init__(self): self.current_index = 0 self.score = 0 self.selected_image = None self.current_correct_index = None self.audio_dir = None self.clip_to_image = {} self.shuffled_clips = [] self.total_questions = 0 def start_quiz(self, audio_set): """Initialize quiz with selected audio set""" self.audio_dir = "audio_clips" if audio_set == "Khmer Script (ខ្មែរ)" else "audio_clips_ko" self.clip_to_image = build_clip_mapping(self.audio_dir) valid_clips = list(self.clip_to_image.keys()) self.shuffled_clips = valid_clips.copy() random.shuffle(self.shuffled_clips) self.total_questions = len(self.shuffled_clips) self.current_index = 0 self.score = 0 self.selected_image = None clip_path, question, image_paths, _ = self.get_current_clip() image_updates = [] for i in range(4): if i < len(image_paths): image_updates.append(gr.Image(value=image_paths[i], visible=True)) else: image_updates.append(gr.Image(visible=False)) return ( gr.update(visible=False), # Hide start screen gr.update(visible=True), # Show quiz screen clip_path, gr.Markdown(f"### {question}"), *image_updates, "" ) def get_current_clip(self): if self.current_index < len(self.shuffled_clips): clip_name = self.shuffled_clips[self.current_index] clip_path = os.path.join(self.audio_dir, clip_name) correct_image = self.clip_to_image[clip_name] all_images = [img for audio, img in self.clip_to_image.items() if audio != clip_name] incorrect_images = random.sample(all_images, min(3, len(all_images))) all_options = [correct_image] + incorrect_images random.shuffle(all_options) correct_index = all_options.index(correct_image) self.current_correct_index = correct_index image_paths = [os.path.join(self.audio_dir, img) for img in all_options] question = f"What did you hear? ({self.current_index + 1}/{self.total_questions})" return clip_path, question, image_paths, correct_index self.current_correct_index = None return None, "Quiz Complete!", [], None def select_image(self, img_index): self.selected_image = img_index return f"✓ Selected option {img_index + 1}" def check_answer(self): if self.selected_image is None: return ( "⚠️ Please select an answer first!", gr.update(), gr.update(), *[gr.update() for _ in range(4)], "" ) correct_index = self.current_correct_index if correct_index is None: final_msg = f"# 🎉 Quiz Complete!\n## Final Score: {self.score}/{self.total_questions}\n\n![Labubu](https://media1.tenor.com/m/l-eWQvZW4LgAAAAC/labubu-cute.gif)\n\nGreat work! 🌟" return ( final_msg, gr.update(), gr.update(), *[gr.update() for _ in range(4)], "" ) is_correct = self.selected_image == correct_index if is_correct: self.score += 1 feedback = f"# 🎉 Correct! 🎉\n\n![Labubu](https://media1.tenor.com/m/l-eWQvZW4LgAAAAC/labubu-cute.gif)\n\nAwesome job! Score: {self.score}/{self.total_questions}" else: feedback = f"# 😊 Oops! Try Again!\n\nDon't worry, you'll get the next one! Score: {self.score}/{self.total_questions}" # Move to next question self.current_index += 1 self.selected_image = None if self.current_index >= self.total_questions: final_msg = f"# 🎉 Quiz Complete!\n## Final Score: {self.score}/{self.total_questions}\n\n![Labubu](https://i.pinimg.com/originals/09/b4/f1/09b4f1c4090cbd1ab8165eec92e501ab.gif)\n\nGreat work! 🌟" return ( final_msg, None, gr.Markdown(""), *[gr.Image(visible=False) for _ in range(4)], "" ) next_clip, next_question, next_images, _ = self.get_current_clip() image_updates = [] for i in range(4): if i < len(next_images): image_updates.append(gr.Image(value=next_images[i], visible=True)) else: image_updates.append(gr.Image(visible=False)) return ( feedback, next_clip, gr.Markdown(f"### {next_question}"), *image_updates, "" ) def reset(self): """Reset quiz to start screen""" self.current_index = 0 self.score = 0 self.selected_image = None self.audio_dir = None self.clip_to_image = {} self.shuffled_clips = [] return ( gr.update(visible=True), # Show start screen gr.update(visible=False), # Hide quiz screen None, gr.Markdown(""), *[gr.Image(visible=False) for _ in range(4)], "", "" ) quiz = QuizApp() def make_select_handler(index): def handler(): return quiz.select_image(index) return handler custom_css = """ .clickable-image { cursor: pointer !important; border: 3px solid transparent !important; border-radius: 8px !important; transition: all 0.3s ease !important; } .clickable-image:hover { border-color: #2563eb !important; transform: scale(1.05) !important; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3) !important; } .feedback-box { padding: 1.5rem !important; border-radius: 0.75rem !important; margin: 1rem 0 !important; text-align: center !important; font-size: 1.2rem !important; } .labubu-celebration { text-align: center !important; margin: 1rem auto !important; max-width: 300px !important; } """ with gr.Blocks(title="Khmer Learning Quiz", theme=gr.themes.Base(), css=custom_css) as demo: gr.Markdown("# 🎵 Khmer Audio Learning Quiz") # Start Screen with gr.Column(visible=True) as start_screen: gr.Markdown("## Welcome! 👋") gr.Markdown("### Choose your learning mode:") audio_set_choice = gr.Radio( choices=["Khmer Script (ខ្មែរ)", "Korean (한글/ko)"], value="Khmer Script (ខ្មែរ)", label="Select Audio Set", info="Choose which set of audio clips you want to practice with" ) start_btn = gr.Button("🚀 Start Quiz", variant="primary", size="lg") # Quiz Screen with gr.Column(visible=False) as quiz_screen: gr.Markdown("Listen to each audio clip and select the correct image!") audio_player = gr.Audio(label="🔊 Listen to the audio clip", autoplay=False) question_text = gr.Markdown("") gr.Markdown("### Click on an image to select:") with gr.Row(): image_options = [] for i in range(4): img = gr.Image(show_label=False, interactive=False, height=200, visible=False, elem_classes="clickable-image") image_options.append(img) selection_status = gr.Textbox(label="Your Selection", value="", interactive=False) with gr.Row(): submit_btn = gr.Button("✓ Submit Answer", variant="primary", size="lg") reset_btn = gr.Button("🔄 Back to Start", variant="secondary") feedback_text = gr.Markdown("", elem_classes="feedback-box") # Event handlers start_btn.click( fn=quiz.start_quiz, inputs=[audio_set_choice], outputs=[start_screen, quiz_screen, audio_player, question_text] + image_options + [selection_status] ) for i in range(4): image_options[i].select(fn=make_select_handler(i), inputs=[], outputs=[selection_status]) submit_btn.click(fn=quiz.check_answer, inputs=[], outputs=[feedback_text, audio_player, question_text] + image_options + [selection_status]) reset_btn.click(fn=quiz.reset, inputs=[], outputs=[start_screen, quiz_screen, audio_player, question_text] + image_options + [selection_status, feedback_text]) if __name__ == "__main__": demo.launch()