Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| from transformers import pipeline # Import for ASR (Speech-to-Text) and TTS (Text-to-Speech) | |
| import torchaudio # Required by some TTS models for audio handling | |
| # Define base paths for assets - IMPORTANT: You need to create these folders and place your files here | |
| # For Hugging Face Spaces, these paths are relative to your app.py file | |
| ASSETS_DIR = "./assets" | |
| IMAGE_DIR = os.path.join(ASSETS_DIR, "images") | |
| AUDIO_DIR = os.path.join(ASSETS_DIR, "audio") # This will also be used for dynamically generated audio | |
| # Create asset directories if they don't exist (for local testing, Hugging Face handles this with git lfs) | |
| # On Hugging Face Spaces, you'll upload these folders with your files. | |
| os.makedirs(IMAGE_DIR, exist_ok=True) | |
| os.makedirs(AUDIO_DIR, exist_ok=True) | |
| # Initialize ASR pipeline for pronunciation check | |
| # Using a small, multilingual Whisper model for demonstration. | |
| # For better Arabic-specific performance, consider a model fine-tuned on Arabic speech. | |
| # device=-1 will try to use GPU if available, otherwise CPU. | |
| try: | |
| asr_pipeline = pipeline("automatic-speech-recognition", model="openai/whisper-small", device=-1) | |
| except Exception as e: | |
| print(f"Warning: Could not load ASR model. Pronunciation check will be a placeholder. Error: {e}") | |
| asr_pipeline = None # Set to None if model loading fails | |
| # Initialize TTS pipeline for audio feedback | |
| # Using a specific Arabic TTS model. | |
| try: | |
| # Ensure this model is suitable for your Hugging Face Space's resources. | |
| # facebook/mms-tts-ara is a good general Arabic TTS model. | |
| tts_pipeline = pipeline("text-to-speech", model="facebook/mms-tts-ara", device=-1) | |
| except Exception as e: | |
| print(f"Warning: Could not load TTS model. Audio feedback will not be generated. Error: {e}") | |
| tts_pipeline = None # Set to None if model loading fails | |
| # Data for Arabic Letters | |
| # Updated paths for word_image and word_audio based on the provided "alphabets pictures names.JPG" | |
| arabic_letters_data = [ | |
| { | |
| "letter": "أ", | |
| "image": os.path.join(IMAGE_DIR, "alif.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "alif.wav"), | |
| "word_example": "أرنب (Arnab - Rabbit)", | |
| "word_image": os.path.join(IMAGE_DIR, "arnab.jpg"), | |
| "word_audio": os.path.join(AUDIO_DIR, "arnab.wav"), | |
| }, | |
| { | |
| "letter": "ب", | |
| "image": os.path.join(IMAGE_DIR, "ba.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ba.wav"), | |
| "word_example": "بطة (Batta - Duck)", | |
| "word_image": os.path.join(IMAGE_DIR, "batta.jpg"), | |
| "word_audio": os.path.join(AUDIO_DIR, "batta.wav"), | |
| }, | |
| { | |
| "letter": "ت", | |
| "image": os.path.join(IMAGE_DIR, "taa.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "taa.wav"), | |
| "word_example": "تفاح (Tuffah - Apple)", | |
| "word_image": os.path.join(IMAGE_DIR, "tuffaha.jpg"), # Corrected from 'placeholder_word_taa.jpg' | |
| "word_audio": os.path.join(AUDIO_DIR, "tuffaha.wav"), # Corrected from 'placeholder_word_taa.wav' | |
| }, | |
| { | |
| "letter": "ث", | |
| "image": os.path.join(IMAGE_DIR, "tha.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "tha.wav"), | |
| "word_example": "ثعلب (Tha'lab - Fox)", | |
| "word_image": os.path.join(IMAGE_DIR, "thalab.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "thalab.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ج", | |
| "image": os.path.join(IMAGE_DIR, "jim.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "jim.wav"), | |
| "word_example": "جمل (Jamal - Camel)", | |
| "word_image": os.path.join(IMAGE_DIR, "jamal.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "jamal.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ح", | |
| "image": os.path.join(IMAGE_DIR, "ha.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ha.wav"), | |
| "word_example": "حصان (Hisan - Horse)", | |
| "word_image": os.path.join(IMAGE_DIR, "hisan.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "hisan.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "خ", | |
| "image": os.path.join(IMAGE_DIR, "kha.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "kha.wav"), | |
| "word_example": "خروف (Kharouf - Sheep)", | |
| "word_image": os.path.join(IMAGE_DIR, "kharouf.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "kharouf.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "د", | |
| "image": os.path.join(IMAGE_DIR, "dal.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "dal.wav"), | |
| "word_example": "دجاجة (Dajaja - Chicken)", | |
| "word_image": os.path.join(IMAGE_DIR, "dajaja.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "dajaja.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ذ", | |
| "image": os.path.join(IMAGE_DIR, "dhal.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "dhal.wav"), | |
| "word_example": "ذرة (Dhurah - Corn)", | |
| "word_image": os.path.join(IMAGE_DIR, "dhurah.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "dhurah.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ر", | |
| "image": os.path.join(IMAGE_DIR, "ra.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ra.wav"), | |
| "word_example": "رمان (Rumman - Pomegranate)", | |
| "word_image": os.path.join(IMAGE_DIR, "rumman.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "rumman.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ز", | |
| "image": os.path.join(IMAGE_DIR, "za.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "za.wav"), | |
| "word_example": "زرافة (Zarafa - Giraffe)", | |
| "word_image": os.path.join(IMAGE_DIR, "zarafa.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "zarafa.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "س", | |
| "image": os.path.join(IMAGE_DIR, "sin.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "sin.wav"), | |
| "word_example": "سمكة (Samaka - Fish)", | |
| "word_image": os.path.join(IMAGE_DIR, "samaka.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "samaka.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ش", | |
| "image": os.path.join(IMAGE_DIR, "shin.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "shin.wav"), | |
| "word_example": "شجرة (Shajara - Tree)", | |
| "word_image": os.path.join(IMAGE_DIR, "shajara.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "shajara.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ص", | |
| "image": os.path.join(IMAGE_DIR, "sad.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "sad.wav"), | |
| "word_example": "صابون (Saboon - Soap)", | |
| "word_image": os.path.join(IMAGE_DIR, "saboon.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "saboon.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ض", | |
| "image": os.path.join(IMAGE_DIR, "dad.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "dad.wav"), | |
| "word_example": "ضفدع (Difda - Frog)", | |
| "word_image": os.path.join(IMAGE_DIR, "difda.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "difda.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ط", | |
| "image": os.path.join(IMAGE_DIR, "ta.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ta.wav"), | |
| "word_example": "طائرة (Ta'ira - Airplane)", | |
| "word_image": os.path.join(IMAGE_DIR, "tairah.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "tairah.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ظ", | |
| "image": os.path.join(IMAGE_DIR, "zay.jpg"), # Using zay.jpg for ظ based on previous mapping, ensure this image is correct for ظ | |
| "audio": os.path.join(AUDIO_DIR, "zay.wav"), # Using zay.wav for ظ, ensure this audio is correct for ظ | |
| "word_example": "ظرف (Zarf - Envelope)", | |
| "word_image": os.path.join(IMAGE_DIR, "zarf.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "zarf.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ع", | |
| "image": os.path.join(IMAGE_DIR, "ayn.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ayn.wav"), | |
| "word_example": "عين (Ayn - Eye)", | |
| "word_image": os.path.join(IMAGE_DIR, "ain.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "ain.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "غ", | |
| "image": os.path.join(IMAGE_DIR, "ghayn.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ghayn.wav"), | |
| "word_example": "غواصة (Ghawwasa - Submarine)", | |
| "word_image": os.path.join(IMAGE_DIR, "ghawwasa.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "ghawwasa.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ف", | |
| "image": os.path.join(IMAGE_DIR, "fa.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "fa.wav"), | |
| "word_example": "فراشة (Farasha - Butterfly)", | |
| "word_image": os.path.join(IMAGE_DIR, "farasha.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "farasha.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ق", | |
| "image": os.path.join(IMAGE_DIR, "qaf.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "qaf.wav"), | |
| "word_example": "قرد (Qird - Monkey)", | |
| "word_image": os.path.join(IMAGE_DIR, "qird.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "qird.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ك", | |
| "image": os.path.join(IMAGE_DIR, "kaf.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "kaf.wav"), | |
| "word_example": "كلب (Kalb - Dog)", | |
| "word_image": os.path.join(IMAGE_DIR, "kalb.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "kalb.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ل", | |
| "image": os.path.join(IMAGE_DIR, "lam.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "lam.wav"), | |
| "word_example": "ليمونة (Laymuna - Lemon)", | |
| "word_image": os.path.join(IMAGE_DIR, "laymuna.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "laymuna.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "م", | |
| "image": os.path.join(IMAGE_DIR, "mim.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "mim.wav"), | |
| "word_example": "موزة (Mawzah - Banana)", | |
| "word_image": os.path.join(IMAGE_DIR, "mawzah.jpg"), # Corrected from 'mauzah.jpg' | |
| "word_audio": os.path.join(AUDIO_DIR, "mawzah.wav"), # Corrected from 'mauzah.wav' | |
| }, | |
| { | |
| "letter": "ن", | |
| "image": os.path.join(IMAGE_DIR, "nun.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "nun.wav"), | |
| "word_example": "نمر (Namir - Tiger)", | |
| "word_image": os.path.join(IMAGE_DIR, "namir.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "namir.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ه", | |
| "image": os.path.join(IMAGE_DIR, "ha.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ha.wav"), | |
| "word_example": "هاتف (Hatif - Telephone)", | |
| "word_image": os.path.join(IMAGE_DIR, "hatif.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "hatif.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "و", | |
| "image": os.path.join(IMAGE_DIR, "waw.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "waw.wav"), | |
| "word_example": "وردة (Warda - Rose)", | |
| "word_image": os.path.join(IMAGE_DIR, "warda.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "warda.wav"), # Corrected | |
| }, | |
| { | |
| "letter": "ي", | |
| "image": os.path.join(IMAGE_DIR, "ya.jpg"), | |
| "audio": os.path.join(AUDIO_DIR, "ya.wav"), | |
| "word_example": "يد (Yad - Hand)", | |
| "word_image": os.path.join(IMAGE_DIR, "yad.jpg"), # Corrected | |
| "word_audio": os.path.join(AUDIO_DIR, "yad.wav"), # Corrected | |
| } | |
| ] | |
| # Placeholder data for Arabic Stories | |
| # YOU MUST REPLACE 'placeholder_story_image_pageX.jpg' and 'placeholder_story_audio.wav' | |
| # with actual paths to your story images and audio files within the 'assets/' directory. | |
| arabic_stories_data = [ | |
| { | |
| "title": "The Gingerbread Man: رجل خبز الزنجبيل (Rajul Khubz Al-Zanjabeel)", | |
| "pages": [ | |
| { | |
| "text": "في يوم من الأيام، خرج أرنب صغير يلعب في الحديقة الخضراء. كان الجو جميلاً والشمس مشرقة.", | |
| "image": os.path.join(IMAGE_DIR, "story1_page1.JPG"), # e.g., 'assets/images/story1_page1.jpg' | |
| "audio": os.path.join(AUDIO_DIR, "story1_page1.wav"), # e.g., 'assets/audio/story1_page1.wav' | |
| }, | |
| { | |
| "text": "فجأة، رأى الأرنب زهرة حمراء جميلة. 'ما أجمل هذه الزهرة!' قال الأرنب بسعادة.", | |
| "image": os.path.join(IMAGE_DIR, "story1_page2.JPG"), # e.g., 'assets/images/story1_page2.jpg' | |
| "audio": os.path.join(AUDIO_DIR, "story1_page2.wav"), # e.g., 'assets/audio/story1_page2.wav' | |
| }, | |
| { | |
| "text": "فجأة، رأى الأرنب زهرة حمراء جميلة. 'ما أجمل هذه الزهرة!' قال الأرنب بسعادة.", | |
| "image": os.path.join(IMAGE_DIR, "story1_page3.JPG"), # e.g., 'assets/images/story1_page3.jpg' | |
| "audio": os.path.join(AUDIO_DIR, "story1_page3.wav"), # e.g., 'assets/audio/story1_page3.wav' | |
| }, # Add more pages for this story. Each page needs text, an image, and corresponding audio. | |
| ] | |
| }, | |
| { | |
| "title": "الأسد والفأر (The Lion and the Mouse)", | |
| "pages": [ | |
| { | |
| "text": "كان أسد نائماً تحت شجرة كبيرة في الغابة. جاء فأر صغير وركض فوق الأسد.", | |
| "image": os.path.join(IMAGE_DIR, "story2_page1.jpg"), # e.g., 'assets/images/story2_page1.jpg' | |
| "audio": os.path.join(AUDIO_DIR, "story2_page1.wav"), # e.g., 'assets/audio/story2_page1.wav' | |
| }, | |
| { | |
| "text": "استيقظ الأسد غاضباً وأمسك بالفأر. توسل الفأر إليه ليتركه، ووعده بمساعدته يوماً ما.", | |
| "image": os.path.join(IMAGE_DIR, "story2_page2.jpg"), # e.g., 'assets/images/story2_page2.jpg' | |
| "audio": os.path.join(AUDIO_DIR, "story2_page2.wav"), # e.g., 'assets/audio/story2_page2.wav' | |
| }, | |
| # Add more pages for this story. | |
| ] | |
| } | |
| ] | |
| # Global state variables for navigation (do not modify directly outside functions) | |
| current_letter_idx = 0 | |
| current_story_idx = 0 | |
| current_story_page_idx = 0 | |
| # --- Functions for Learning Arabic Letters Section --- | |
| def get_current_letter_content(): | |
| """ | |
| Retrieves the current letter's data (image, audio, word example, word image, word audio) | |
| based on the global `current_letter_idx`. The letter text is no longer returned directly. | |
| """ | |
| if not arabic_letters_data: | |
| # Handle case where no letter data is available | |
| return None, None, None, None # Adjusted return for consistency | |
| data = arabic_letters_data[current_letter_idx] | |
| # Return (letter_image, letter_audio, word_image, word_audio) | |
| # word_example is no longer returned as an output to Gradio components | |
| return ( | |
| data["image"], | |
| data["audio"], # This audio is for the letter sound itself | |
| data["word_image"], | |
| data["word_audio"] # This audio is for the word example sound | |
| ) | |
| def next_letter_func(): | |
| """ | |
| Advances to the next Arabic letter in the `arabic_letters_data` list. | |
| If at the end, it loops back to the first letter. | |
| """ | |
| global current_letter_idx | |
| if current_letter_idx < len(arabic_letters_data) - 1: | |
| current_letter_idx += 1 | |
| else: | |
| current_letter_idx = 0 # Loop back to the beginning | |
| return get_current_letter_content() | |
| def prev_letter_func(): | |
| """ | |
| Goes back to the previous Arabic letter in the `arabic_letters_data` list. | |
| If at the beginning, it loops to the last letter. | |
| """ | |
| global current_letter_idx | |
| if current_letter_idx > 0: | |
| current_letter_idx -= 1 | |
| else: | |
| current_letter_idx = len(arabic_letters_data) - 1 # Loop to the end | |
| return get_current_letter_content() | |
| def play_audio(audio_path): | |
| """ | |
| Plays an audio file. This function is called by Gradio's Audio component. | |
| It checks if the file exists before returning the path. | |
| """ | |
| if os.path.exists(audio_path): | |
| return audio_path | |
| print(f"Error: Audio file not found at {audio_path}") | |
| # Returning None for audio will result in an error message in Gradio's console | |
| return None | |
| def generate_tts_audio(text_to_speak, filename="temp_feedback_audio.wav"): | |
| """ | |
| Generates audio from text using the TTS pipeline and saves it to a file. | |
| Returns the file path. | |
| """ | |
| if tts_pipeline is None: | |
| print("TTS pipeline not loaded, cannot generate audio.") | |
| return None | |
| try: | |
| # Generate speech | |
| # For 'facebook/mms-tts-ara', the audio output might need specific handling. | |
| # This is a common pattern for Hugging Face TTS pipelines. | |
| output = tts_pipeline(text_to_speak) | |
| # Assuming output is a dictionary like {'audio': numpy_array, 'sampling_rate': int} | |
| audio_array = output['audio'] | |
| sampling_rate = output['sampling_rate'] | |
| # Save the audio to a temporary file | |
| output_path = os.path.join(AUDIO_DIR, filename) | |
| torchaudio.save(output_path, audio_array.unsqueeze(0), sampling_rate) | |
| return output_path | |
| except Exception as e: | |
| print(f"Error generating TTS audio for '{text_to_speak}': {e}") | |
| return None | |
| def check_pronunciation(audio_input): | |
| """ | |
| Performs pronunciation check using ASR. | |
| Compares transcribed text to the expected Arabic letter. | |
| Returns text feedback and an audio file for the feedback. | |
| """ | |
| feedback_text = "" | |
| feedback_audio_path = None | |
| if audio_input is None: | |
| feedback_text = "من فضلك سجل صوتك أولاً. (Please record your voice first.)" | |
| feedback_audio_path = generate_tts_audio("من فضلك سجل صوتك أولاً.") | |
| return feedback_text, feedback_audio_path | |
| if asr_pipeline is None: | |
| feedback_text = "وظيفة التحقق من النطق غير متوفرة. (Pronunciation check not available. ASR model failed to load.)" | |
| feedback_audio_path = generate_tts_audio("وظيفة التحقق من النطق غير متوفرة.") | |
| return feedback_text, feedback_audio_path | |
| try: | |
| # Transcribe the audio input | |
| # Ensure the language is explicitly set to Arabic for better results with Whisper | |
| transcription_result = asr_pipeline(audio_input, generate_kwargs={"language": "ar"}) | |
| transcribed_text = transcription_result["text"].strip().lower() | |
| # Get the expected Arabic letter for comparison | |
| expected_letter = arabic_letters_data[current_letter_idx]["letter"].lower() | |
| # Simple check: does the transcription contain the expected letter? | |
| if expected_letter in transcribed_text: | |
| feedback_text = f"أحسنت! (Excellent!) لقد قلت: '{transcribed_text}'" | |
| feedback_audio_path = generate_tts_audio("أحسنت!") | |
| else: | |
| feedback_text = f"حاول مرة أخرى. (Try again.) لقد قلت: '{transcribed_text}'" \ | |
| f" كان المتوقع: '{arabic_letters_data[current_letter_idx]['letter']}'" | |
| feedback_audio_path = generate_tts_audio("حاول مرة أخرى.") | |
| return feedback_text, feedback_audio_path | |
| except Exception as e: | |
| feedback_text = f"حدث خطأ أثناء التحقق من النطق: {e}. (An error occurred during pronunciation check.)" | |
| feedback_audio_path = generate_tts_audio("حدث خطأ أثناء التحقق من النطق.") # Generic error audio | |
| return feedback_text, feedback_audio_path | |
| # --- Functions for Arabic Storytelling Section --- | |
| def get_story_titles(): | |
| """ | |
| Returns a list of story titles to populate the dropdown menu. | |
| """ | |
| return [story["title"] for story in arabic_stories_data] | |
| def select_story_func(title): | |
| """ | |
| Selects a story based on its title from the dropdown. | |
| Resets the page index to 0 for the newly selected story. | |
| """ | |
| global current_story_idx, current_story_page_idx | |
| for i, story in enumerate(arabic_stories_data): | |
| if story["title"] == title: | |
| current_story_idx = i | |
| current_story_page_idx = 0 # Reset to the first page of the new story | |
| break | |
| # After selecting, return the content of the first page of the chosen story | |
| return get_current_story_page_content() | |
| def get_current_story_page_content(): | |
| """ | |
| Retrieves the content (title, text, image, audio) for the current page | |
| of the currently selected story. | |
| """ | |
| if not arabic_stories_data: | |
| # Handle case where no story data is available | |
| return "لا توجد قصص متاحة", None, None, None | |
| story = arabic_stories_data[current_story_idx] | |
| if current_story_page_idx < len(story["pages"]): | |
| # Return content for the current page | |
| page = story["pages"][current_story_page_idx] | |
| return story["title"], page["text"], page["image"], page["audio"] | |
| else: | |
| # If past the last page, indicate the end of the story | |
| return story["title"], "انتهت القصة! (End of story!)", None, None # No image/audio at end | |
| def next_story_page_func(): | |
| """ | |
| Advances to the next page of the current story. | |
| If at the end of the story, it stays on the last page. | |
| """ | |
| global current_story_page_idx | |
| story = arabic_stories_data[current_story_idx] | |
| if current_story_page_idx < len(story["pages"]) - 1: | |
| current_story_page_idx += 1 | |
| # If at the last page, do nothing (stay on the last page) | |
| return get_current_story_page_content() | |
| def prev_story_page_func(): | |
| """ | |
| Goes back to the previous page of the current story. | |
| If at the beginning of the story, it loops to the last letter. | |
| """ | |
| global current_story_page_idx | |
| if current_story_page_idx > 0: | |
| current_story_page_idx -= 1 | |
| # If at the first page, do nothing (stay on the first page) | |
| return get_current_story_page_content() | |
| # --- Gradio UI Definition using gr.Blocks for advanced layout and styling --- | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(), # A soft, pleasant theme | |
| # Custom CSS for better aesthetics and responsiveness | |
| css=""" | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .gradio-container { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f0f8ff; /* Light blue background for the entire app */ | |
| border-radius: 15px; | |
| box-shadow: 0 8px 16px rgba(0,0,0,0.2); /* Enhanced shadow */ | |
| padding: 25px; | |
| max-width: 950px; /* Slightly wider max-width */ | |
| margin: 30px auto; | |
| border: 2px solid #aaddff; /* Light blue border */ | |
| } | |
| h1, h2, h3 { | |
| color: #2c3e50; /* Darker blue-grey for headings */ | |
| text-align: center; | |
| margin-bottom: 25px; | |
| font-weight: 700; /* Bold headings */ | |
| } | |
| .gr-tab-item { | |
| border-radius: 12px 12px 0 0 !important; /* Rounded top corners for tabs */ | |
| background-color: #e6f7ff; /* Lighter tab background */ | |
| font-size: 1.15em; /* Larger tab text */ | |
| padding: 12px 20px; | |
| transition: background-color 0.3s ease; | |
| } | |
| .gr-tab-item.selected { | |
| background-color: #ffffff !important; | |
| border-bottom: none !important; | |
| color: #007bff !important; /* Vibrant blue for selected tab */ | |
| font-weight: bold; | |
| box-shadow: 0 -3px 8px rgba(0,0,0,0.1); /* Shadow for selected tab */ | |
| } | |
| .gr-button { | |
| background-color: #007bff; /* Primary blue button */ | |
| color: white; | |
| border-radius: 12px; /* More rounded buttons */ | |
| padding: 12px 25px; | |
| font-size: 1.15em; /* Larger button text */ | |
| margin: 8px; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.25); /* More prominent shadow */ | |
| transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease; | |
| border: none; /* No default border */ | |
| cursor: pointer; | |
| } | |
| .gr-button:hover { | |
| background-color: #0056b3; /* Darker blue on hover */ | |
| transform: translateY(-3px); /* Lift effect on hover */ | |
| box-shadow: 0 6px 12px rgba(0,0,0,0.3); /* Larger shadow on hover */ | |
| } | |
| .gr-image { | |
| border-radius: 18px; /* More rounded images */ | |
| overflow: hidden; | |
| border: 3px solid #aaddff; /* Thicker light blue border */ | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.15); | |
| background-color: #ffffff; | |
| display: block; /* Ensure image is a block element for centering */ | |
| margin: 15px auto; /* Center images */ | |
| } | |
| .gr-audio { | |
| border-radius: 12px; | |
| border: 2px solid #cceeff; | |
| background-color: #ffffff; | |
| padding: 10px; | |
| margin: 10px 0; | |
| } | |
| .gr-markdown { | |
| background-color: #ffffff; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 15px; | |
| border: 2px solid #cceeff; | |
| text-align: center; | |
| font-size: 1.3em; /* Larger text for readability for kids */ | |
| color: #333; | |
| line-height: 1.6; /* Improve line spacing */ | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.08); | |
| } | |
| .gr-dropdown { | |
| border-radius: 12px; | |
| padding: 8px; | |
| font-size: 1.1em; | |
| border: 1px solid #cceeff; | |
| box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); | |
| } | |
| .feedback-message { | |
| color: #28a745; /* Green for success/positive feedback */ | |
| font-weight: bold; | |
| text-align: center; | |
| margin-top: 20px; | |
| font-size: 1.2em; | |
| } | |
| .error-message { | |
| color: #dc3545; /* Red for error messages */ | |
| font-weight: bold; | |
| text-align: center; | |
| margin-top: 20px; | |
| font-size: 1.2em; | |
| } | |
| hr { | |
| border: 0; | |
| height: 1px; | |
| background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 123, 255, 0.75), rgba(0, 0, 0, 0)); | |
| margin: 30px 0; | |
| } | |
| [dir="rtl"] .gr-button, [dir="rtl"] .gr-markdown, [dir="rtl"] .gr-dropdown { | |
| text-align: right; /* Ensure RTL alignment for text within components */ | |
| } | |
| """ | |
| ) as demo: | |
| # Main application title and welcoming message | |
| gr.Markdown("# <span style='color:#007bff;'>تطبيق تعلم العربية للأطفال</span>", rtl=True) | |
| # Add logo image | |
| gr.Image( | |
| value=os.path.join(IMAGE_DIR, "Applogo.png"), # Path to your logo image | |
| label="شعار التطبيق (App Logo)", | |
| width=300, | |
| height=150, | |
| interactive=False, | |
| elem_classes=["gr-image"] | |
| ) | |
| gr.Markdown("## <span style='color:#34495e;'>مرحباً يا أبطالنا الصغار! (Welcome, Little Heroes!)</span>", rtl=True) | |
| # Tabs for different learning modes | |
| with gr.Tabs(): | |
| # --- Arabic Letters Learning Tab --- | |
| with gr.TabItem("تعلم الحروف العربية (Learn Arabic Letters)"): | |
| with gr.Column( | |
| scale=1, | |
| min_width=400, | |
| elem_id="letter-learning-section", | |
| variant="panel" | |
| ): | |
| gr.Markdown("### <span style='color:#28a745;'>تعلم حرفاً جديداً كل يوم! (Learn a new letter every day!)</span>", rtl=True) | |
| # The letter image will now be the primary display for the letter. | |
| letter_image = gr.Image( | |
| label="صورة الحرف (Letter Image)", | |
| width=250, height=250, | |
| interactive=False, | |
| elem_classes=["gr-image"] | |
| ) | |
| # Hidden audio component to play the letter sound | |
| letter_audio_output = gr.Audio( | |
| label="صوت الحرف (Letter Sound)", | |
| autoplay=True, | |
| type="filepath", | |
| visible=False | |
| ) | |
| # Navigation buttons for letters | |
| with gr.Row(): | |
| prev_letter_btn = gr.Button("الحرف السابق (Previous Letter)", elem_classes=["gr-button"]) | |
| play_letter_btn = gr.Button("استمع للحرف (Listen to Letter)", elem_classes=["gr-button"]) | |
| next_letter_btn = gr.Button("الحرف التالي (Next Letter)", elem_classes=["gr-button"]) | |
| #gr.Markdown("<hr>", elem_classes=["gr-markdown"]) | |
| # --- Word Example Section for the current letter --- | |
| gr.Markdown("### <span style='color:#ffc107;'>كلمات تبدأ بهذا الحرف (Words starting with this letter)</span>", rtl=True) | |
| # Display image for the example word | |
| word_image = gr.Image( | |
| label="صورة الكلمة (Word Image)", | |
| width=250, height=250, | |
| interactive=False, | |
| elem_classes=["gr-image"] | |
| ) | |
| # Hidden audio component to play the word sound | |
| word_audio_output = gr.Audio( | |
| label="صوت الكلمة (Word Sound)", | |
| autoplay=True, | |
| type="filepath", | |
| visible=False | |
| ) | |
| # Button to play the word audio | |
| play_word_btn = gr.Button("استمع للكلمة (Listen to Word)", elem_classes=["gr-button"]) | |
| gr.Markdown("<hr>", elem_classes=["gr-markdown"]) | |
| # --- Pronunciation Practice Section --- | |
| gr.Markdown("### <span style='color:#17a2b8;'>تدرب على النطق (Practice Pronunciation)</span>", rtl=True) | |
| # Microphone input for user to record pronunciation | |
| pronunciation_input = gr.Audio( | |
| sources=["microphone"], | |
| type="filepath", | |
| label="سجل صوتك وأنت تنطق الحرف/الكلمة (Record your voice saying the letter/word)", | |
| elem_classes=["gr-audio"] | |
| ) | |
| # Markdown to display feedback on pronunciation | |
| pronunciation_feedback = gr.Markdown( | |
| label="التقييم (Feedback)", | |
| rtl=True, | |
| elem_classes=["gr-markdown"] | |
| ) | |
| # New Audio component to play pronunciation feedback | |
| pronunciation_feedback_audio = gr.Audio( | |
| label="صوت التقييم (Feedback Audio)", | |
| autoplay=True, | |
| type="filepath", | |
| visible=False # Keep it hidden, as we only trigger playback | |
| ) | |
| # Button to trigger the pronunciation check | |
| check_pronunciation_btn = gr.Button("تحقق من النطق (Check Pronunciation)", elem_classes=["gr-button"]) | |
| # --- Arabic Storytelling Tab --- | |
| with gr.TabItem("قصص عربية (Arabic Storytelling)"): | |
| with gr.Column( | |
| scale=1, | |
| min_width=400, | |
| elem_id="storytelling-section", | |
| variant="panel" | |
| ): | |
| gr.Markdown("### <span style='color:#fd7e14;'>استمتع بقصصنا الشيقة! (Enjoy our exciting stories!)</span>", rtl=True) | |
| # Dropdown to select a story | |
| story_selection_dropdown = gr.Dropdown( | |
| choices=get_story_titles(), | |
| label="اختر قصة (Choose a Story)", | |
| value=get_story_titles()[0] if get_story_titles() else None, | |
| elem_classes=["gr-dropdown"] | |
| ) | |
| # Display area for story title | |
| story_title_display = gr.Markdown( | |
| value="", | |
| label="عنوان القصة (Story Title)", | |
| rtl=True, | |
| elem_classes=["gr-markdown"] | |
| ) | |
| # Display area for story text | |
| story_text_display = gr.Markdown( | |
| value="", | |
| label="نص القصة (Story Text)", | |
| rtl=True, | |
| elem_classes=["gr-markdown"] | |
| ) | |
| # Display image for the current story page | |
| story_image_display = gr.Image( | |
| label="صورة القصة (Story Image)", | |
| width=450, height=350, | |
| interactive=False, | |
| elem_classes=["gr-image"] | |
| ) | |
| # Hidden audio component to play story page audio | |
| story_audio_output = gr.Audio( | |
| label="صوت القصة (Story Audio)", | |
| autoplay=True, | |
| type="filepath", | |
| visible=False | |
| ) | |
| # Navigation buttons for story pages | |
| with gr.Row(): | |
| prev_story_page_btn = gr.Button("الصفحة السابقة (Previous Page)", elem_classes=["gr-button"]) | |
| play_story_btn = gr.Button("استمع للقصة (Listen to Story)", elem_classes=["gr-button"]) | |
| next_story_page_btn = gr.Button("الصفحة التالية (Next Page)", elem_classes=["gr-button"]) | |
| # --- Event Handlers (Logic for button clicks and component changes) --- | |
| # --- Event Handlers for Learning Arabic Letters Tab --- | |
| # When the demo loads, display the content of the first letter | |
| demo.load( | |
| get_current_letter_content, | |
| inputs=None, | |
| outputs=[ | |
| letter_image, | |
| letter_audio_output, | |
| word_image, | |
| word_audio_output | |
| ], | |
| queue=False | |
| ) | |
| # Connect navigation buttons to their respective functions | |
| next_letter_btn.click( | |
| next_letter_func, | |
| inputs=None, | |
| outputs=[ | |
| letter_image, | |
| letter_audio_output, | |
| word_image, | |
| word_audio_output | |
| ] | |
| ) | |
| prev_letter_btn.click( | |
| prev_letter_func, | |
| inputs=None, | |
| outputs=[ | |
| letter_image, | |
| letter_audio_output, | |
| word_image, | |
| word_audio_output | |
| ] | |
| ) | |
| # Connect play audio buttons. Using lambda to pass the current audio path dynamically. | |
| play_letter_btn.click( | |
| lambda: play_audio(arabic_letters_data[current_letter_idx]["audio"]), | |
| inputs=None, | |
| outputs=letter_audio_output | |
| ) | |
| play_word_btn.click( | |
| lambda: play_audio(arabic_letters_data[current_letter_idx]["word_audio"]), | |
| inputs=None, | |
| outputs=word_audio_output | |
| ) | |
| # Connect pronunciation check button | |
| check_pronunciation_btn.click( | |
| check_pronunciation, | |
| inputs=pronunciation_input, | |
| outputs=[pronunciation_feedback, pronunciation_feedback_audio] | |
| ) | |
| # --- Event Handlers for Arabic Storytelling Tab --- | |
| # When a new story is selected from the dropdown | |
| story_selection_dropdown.change( | |
| select_story_func, | |
| inputs=story_selection_dropdown, | |
| outputs=[story_title_display, story_text_display, story_image_display, story_audio_output] | |
| ) | |
| # Connect navigation buttons for story pages | |
| next_story_page_btn.click( | |
| next_story_page_func, | |
| inputs=None, | |
| outputs=[story_title_display, story_text_display, story_image_display, story_audio_output] | |
| ) | |
| prev_story_page_btn.click( | |
| prev_story_page_func, | |
| inputs=None, | |
| outputs=[story_title_display, story_text_display, story_image_display, story_audio_output] | |
| ) | |
| # Connect button to play the current story page's audio | |
| play_story_btn.click( | |
| lambda: play_audio(arabic_stories_data[current_story_idx]["pages"][current_story_page_idx]["audio"]), | |
| inputs=None, | |
| outputs=story_audio_output | |
| ) | |
| # Launch the Gradio application | |
| demo.launch() | |