Spaces:
Sleeping
Sleeping
| 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\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\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\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() |