import os import re import wave import tempfile from functools import lru_cache import gradio as gr from huggingface_hub import hf_hub_download from piper import PiperVoice, SynthesisConfig from groq import Groq # ============================================================ # 1. CONFIGURATION # ============================================================ GROQ_API_KEY = os.environ.get("GROQ_API_KEY") client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None REPO_ID = "rhasspy/piper-voices" # ============================================================ # 2. LATIFA AI — SYSTEM PROMPT # ============================================================ LATIFA_PROMPT = ( "You are Latifa AI — the official voice guide of 'Under the Palm Tree' " "(تحت شجرة النخيل), an interactive English learning project set in Oman, 1973.\n\n" "IDENTITY:\n" "- You are an Omani educational guide created by Thuraya Mohammed Ali Al-Naabia.\n" "- You are warm, professional, knowledgeable, and encouraging.\n" "- You speak naturally like a real Omani English teacher.\n\n" "═══════════════════════════════════════\n" "LANGUAGE RULES (CRITICAL — FOLLOW STRICTLY):\n" "═══════════════════════════════════════\n" "- Detect the user's language automatically.\n" "- If the user writes in Arabic → respond ENTIRELY in Arabic. No English words mixed in.\n" "- If the user writes in English → respond ENTIRELY in English. No Arabic words mixed in.\n" "- If the user writes in mixed Arabic-English → respond in the SAME language the user used MOST.\n" "- NEVER switch languages mid-sentence. Pick ONE language per response.\n" "- When responding in Arabic, you MUST add full tashkeel (diacritics) to ALL Arabic words.\n" " Example: مَرْحَبًا بِكُمْ فِي تَحْتَ شَجَرَةِ النَّخِيل\n" " The tashkeel is essential for correct pronunciation by the text-to-speech system.\n" "- When responding in English, use simple clear English suitable for language learners.\n\n" "PROJECT:\n" "- Name: 123 Let's Learn English Under The Palm Tree\n" "- Founder: Thuraya Mohammed Ali Al-Naabia — Omani English Teacher & Educational Innovator\n" "- Website: www.under-palm-tree.com\n" "- Setting: Oman, 1973, Al-Qurawashiyah Village, Samail\n" "- The project blends English learning with Omani heritage through interactive storytelling, " "games, and cultural exploration.\n\n" "MAIN CHARACTERS:\n" "- Thuraya: Omani English teacher, black hijab, black abaya/blazer, story narrator. " "Mature, natural, warm.\n" "- John: British male teacher, tall, brown hair, light beard, beige shirt, brown trousers. " "Professional, kind.\n" "- Sophia: British female teacher, blonde hair/bun, green eyes, soft blouse, long skirt. " "Friendly, gentle, curious.\n" "- Malood: Small white baby goat, funny, mischievous. Eats Sophia's ring, chews notebooks.\n\n" "LOCATIONS:\n" "- Al-Qurawashiyah Village, Samail: Traditional Omani village with palm groves, " "falaj irrigation, mud houses, old school.\n" "- Seeb Airport (Muscat): Where John and Sophia first arrive.\n" "- Dukkan Al-Hillah: The village shop.\n" "- Samail Market (Souq): Local market visited via Uncle Nasser's pickup.\n" "- Falaj: Traditional Omani water channels, UNESCO heritage.\n\n" "CHAPTERS:\n" "1. 'Thuraya's Book of Thoughts' — Turkey, 2026. Thuraya remembers: John reading the letter, " "Sophia's hesitation, the flight, arrival in Oman.\n" "2. 'The First Walk in the Village' — Oman, 1973. Sophia's first night in the clay house. " "Morning walk: letj game, falaj, Sadoo's shop, Khamis, nahsher trees.\n" "3. 'The First Day at School' — John writes 'HELLO' on chalkboard. Malood eats Sophia's ring " "and chews notebooks.\n" "4. 'The Morning Departure to the Souq' — Trip to Dukkan Al-Hillah, Uncle Nasser's pickup, " "Samail market. Learning 'I want.' Meeting Habbabouh Saif.\n\n" "GAMES:\n" "- Palm Echo: Speaking and pronunciation practice. Listen, repeat, improve confidence.\n\n" "SKILLS: Speaking, listening, reading, writing, vocabulary, grammar, pronunciation, spelling.\n\n" "CULTURAL NOTES:\n" "- Falaj: Traditional irrigation channels, UNESCO heritage.\n" "- Oud & Luban: Fragrant incense (oud wood, frankincense).\n" "- Letj: Traditional Omani children's game.\n" "- 1973: Oman modernizing under Sultan Qaboos.\n\n" "AUTHOR:\n" "Thuraya Mohammed Ali Al-Naabia — Omani English teacher, educational innovator, storyteller.\n" "Quote: 'Learning is not only about mastering a language; it is about discovering stories, " "building connections, and creating new possibilities.'\n\n" "WEBSITE PAGES: Home, Characters, Chapters, Games, Skills Practice, " "Gallery, Music, Story World, Latifa AI. Guide users to pages when asked.\n\n" "STORY RULES:\n" "- Never invent story events or change character identities.\n" "- Never change the era (always 1973) or add modern objects.\n" "- If you don't know something, say: 'I don't know that part of the story yet.'\n\n" "RESPONSE FORMATTING:\n" "- Do NOT use emojis in your responses.\n" "- Do NOT use markdown formatting (no **, ##, bullets with special chars).\n" "- Write plain text only — the response will be spoken aloud by a voice system.\n" "- Keep answers clear, natural, and conversational.\n" "- For vocabulary: word → meaning in the other language → example sentence.\n" "- For quizzes: ask one question at a time.\n" "- Be culturally respectful and proud of Omani heritage.\n" "- Maximum 3-4 sentences per response to keep it conversational." ) # ============================================================ # 3. VOICE CONFIGURATION & LOADER # ============================================================ VOICE_MAP = { "en": { "model": "en/en_GB/alba/medium/en_GB-alba-medium.onnx", "config": "en/en_GB/alba/medium/en_GB-alba-medium.onnx.json", }, "ar": { "model": "ar/ar_JO/khalil/medium/ar_JO-khalil-medium.onnx", "config": "ar/ar_JO/khalil/medium/ar_JO-khalil-medium.onnx.json", }, } @lru_cache(maxsize=4) def load_piper_voice(model_file, config_file): """Download and cache a Piper voice model from Hugging Face.""" try: mpath = hf_hub_download( repo_id=REPO_ID, filename=model_file, repo_type="model" ) cpath = hf_hub_download( repo_id=REPO_ID, filename=config_file, repo_type="model" ) return PiperVoice.load(mpath, cpath) except Exception as e: print(f"[Latifa] Voice load failed: {e}") return None def detect_lang(text): """Return 'ar' if text is predominantly Arabic, else 'en'.""" if not text: return "en" arabic_chars = 0 total_chars = 0 for c in text: if "\u0600" <= c <= "\u06FF" or "\u0750" <= c <= "\u077F" or "\u064B" <= c <= "\u065F": arabic_chars += 1 if c.isalpha(): total_chars += 1 if total_chars == 0: return "en" return "ar" if arabic_chars / total_chars > 0.3 else "en" def clean_text_for_tts(text, lang): """Clean text before sending to Piper TTS.""" if not text: return "" text = re.sub(r'https?://\S+', '', text) text = re.sub(r'\S+@\S+', '', text) emoji_pattern = re.compile( "[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF" "\U0001F680-\U0001F6FF" "\U0001F1E0-\U0001F1FF" "\U00002702-\U000027B0" "\U000024C2-\U0001F251" "\U0001f926-\U0001f937" "\U00010000-\U0010ffff" "\u2640-\u2642" "\u2600-\u2B55" "\u200d" "\u23cf" "\u23e9" "\u231a" "\ufe0f" "\u3030" "]+", flags=re.UNICODE, ) text = emoji_pattern.sub('', text) text = re.sub(r'\*+', '', text) text = re.sub(r'#+\s*', '', text) text = re.sub(r'`+', '', text) text = re.sub(r'_{2,}', '', text) text = re.sub(r'-{2,}', '', text) text = re.sub(r'\|', '', text) text = re.sub(r'[{}[\]<>()@#$%^&*=+~\\]', '', text) if lang == "ar": text = re.sub( r'[^\u0600-\u06FF\u0750-\u077F\u064B-\u065F\u0670\s.,!?;:،؟!\n]', ' ', text, ) else: text = re.sub(r'[^a-zA-Z\s.,!?;:\'"\-\n]', ' ', text) text = re.sub(r'\s+', ' ', text).strip() max_chars = 800 if len(text) > max_chars: text = text[:max_chars].rsplit(' ', 1)[0] return text def speak(text, lang="en"): """Synthesize speech with Piper. Returns file path or None.""" clean = clean_text_for_tts(text, lang) if not clean or len(clean) < 2: return None cfg = VOICE_MAP.get(lang, VOICE_MAP["en"]) voice = load_piper_voice(cfg["model"], cfg["config"]) if voice is None: return None try: syn = SynthesisConfig( length_scale=1.0, noise_scale=0.667, noise_w_scale=0.8, ) tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.close() with wave.open(tmp.name, "wb") as wf: voice.synthesize_wav(clean, wf, syn_config=syn) return tmp.name except Exception as e: print(f"[Latifa] TTS error: {e}") return None # ============================================================ # 4. KNOWLEDGE BASE # ============================================================ KB = { "characters": { "thuraya": ( "Thuraya — Omani English teacher, story narrator. Black hijab, " "black abaya/blazer. Mature, natural, warm. Leads the story and " "teaches English in Al-Qurawashiyah village." ), "john": ( "John — British male teacher. Tall, brown hair, light beard. " "Beige shirt, brown trousers. Professional and kind. Comes to " "Oman to teach English." ), "sophia": ( "Sophia — British female teacher. Blonde hair in a bun, green eyes. " "Soft blouse, long skirt. Friendly, gentle, curious about Omani culture. " "Her ring gets eaten by Malood!" ), "malood": ( "Malood — Small white baby goat. Funny and mischievous. Famous for " "eating Sophia's ring and chewing notebooks. A beloved chaos-maker." ), }, "chapters": { "1": ( "Chapter 1: 'Thuraya's Book of Thoughts' — Turkey, 2026. Thuraya " "opens her notebook and remembers: John reading the letter in London, " "Sophia's hesitation, the long flight, the heat of Oman's arrival, " "and their first meeting at the airport." ), "2": ( "Chapter 2: 'The First Walk in the Village' — Oman, 1973, late afternoon. " "Sophia's first night in the clay house with oud and luban. Morning walk " "through the village: letj game, falaj, Sadoo's shop, Khamis the " "Thursday-named man, nahsher trees." ), "3": ( "Chapter 3: 'The First Day at School' — Al-Qurawashiyah, 1973. John " "writes 'HELLO' on the chalkboard and meets students. Malood the goat " "eats Sophia's ring and chews the notebooks!" ), "4": ( "Chapter 4: 'The Morning Departure to the Souq' — Trip to Dukkan " "Al-Hillah, then Uncle Nasser's pickup to Samail market. Class learns " "'I want.' Meet the unforgettable Habbabouh Saif." ), }, "locations": { "al-qurawashiyah": ( "Al-Qurawashiyah Village, Samail, Oman — Traditional Omani village " "with palm groves, falaj irrigation, mud/clay houses, old school." ), "seeb": "Seeb Airport (Muscat) — Where John and Sophia first arrive in 1973.", "falaj": ( "Falaj — Traditional Omani irrigation channels carrying mountain water " "to villages. UNESCO heritage." ), "souq": ( "Samail Market (Souq) — Local market visited via Uncle Nasser's pickup." ), "dukkan": "Dukkan Al-Hillah — The village shop in Al-Qurawashiyah.", }, "games": { "palm echo": ( "Palm Echo — Speaking and pronunciation practice game. Listen to words, " "repeat them, build confidence. Skills: pronunciation, speaking, listening." ), }, "skills": { "speaking": "Speaking Practice — Conversations, repetition exercises, pronunciation drills. Try Palm Echo.", "listening": "Listening Practice — Audio activities and story narration.", "reading": "Reading Practice — Story chapters and interactive texts set in Oman.", "writing": "Writing Practice — Guided exercises and creative prompts.", "vocabulary": "Vocabulary — Contextual learning tied to Omani culture and daily life.", "grammar": "Grammar — Rules learned through story examples.", "pronunciation": "Pronunciation — Palm Echo game and repetition exercises.", "spelling": "Spelling — Word games and activities.", }, "cultural": { "falaj": "Falaj: Traditional Omani irrigation — channels from mountains to villages. UNESCO heritage.", "oud": "Oud: Fragrant wood incense, deeply valued in Omani homes.", "luban": "Luban (Frankincense): Traditional Omani aromatic resin. Oman was the world's main source.", "letj": "Letj: Traditional Omani children's game played in villages.", "1973": ( "Oman 1973: Modernizing under Sultan Qaboos. Traditional village life " "coexisting with development. Foreign teachers building the education system." ), }, "author": { "name": "Thuraya Mohammed Ali Al-Naabia", "title": "Founder of Under the Palm Tree", "role": "Omani English Teacher & Educational Innovator", "bio": ( "An Omani English teacher dedicated to transforming language learning " "through creativity and technology. Combines storytelling, cultural " "heritage, digital learning, and AI." ), "vision": "English learning as an immersive journey connecting language, culture, creativity.", "philosophy": "The most effective learning happens when students are emotionally connected.", "quote": ( "Learning is not only about mastering a language; it is about discovering " "stories, building connections, and creating new possibilities." ), }, "sitemap": { "home": "https://www.under-palm-tree.com — Project introduction and mission", "characters": "https://www.under-palm-tree.com/palm-characters — Story characters", "chapters": "https://www.under-palm-tree.com/underthepalm — Read the story chapters", "games": "https://www.under-palm-tree.com/palmgames — Interactive educational games", "skills": "https://www.under-palm-tree.com/four-and-more-skills — Skills practice hub", "gallery": "https://www.under-palm-tree.com/gallery-1 — Visual gallery", "music": "https://www.under-palm-tree.com/songs — Music and audio", }, } # ============================================================ # 5. KNOWLEDGE SEARCH # ============================================================ QUERY_KEYWORDS = { "character": "characters", "who is": "characters", "john": "characters", "sophia": "characters", "malood": "characters", "chapter": "chapters", "story": "chapters", "episode": "chapters", "game": "games", "play": "games", "echo": "games", "palm echo": "games", "practice": "skills", "skill": "skills", "learn": "skills", "speaking": "skills", "listening": "skills", "reading": "skills", "writing": "skills", "vocab": "skills", "grammar": "skills", "pronunciation": "skills", "spelling": "skills", "author": "author", "founder": "author", "creator": "author", "thuraya mohammed": "author", "oman": "cultural", "culture": "cultural", "1973": "cultural", "falaj": "cultural", "oud": "cultural", "luban": "cultural", "letj": "cultural", "page": "sitemap", "website": "sitemap", "navigate": "sitemap", "where": "sitemap", "link": "sitemap", "show me": "sitemap", "village": "locations", "location": "locations", "samail": "locations", "airport": "locations", "souq": "locations", "shop": "locations", "ثريا": "characters", "صوفيا": "characters", "معلود": "characters", "الماعز": "characters", "شخصية": "characters", "شخصيات": "characters", "فصل": "chapters", "قصة": "chapters", "حكاية": "chapters", "لعبة": "games", "بالم ايكو": "games", "صدى النخلة": "games", "تمرين": "skills", "تعلم": "skills", "ممارسة": "skills", "كتابة": "skills", "قواعد": "skills", "تهجئة": "skills", "مؤلف": "author", "المؤسسة": "author", "المعلمة": "author", "ثريا محمد": "author", "عمان": "cultural", "ثقافة": "cultural", "تراث": "cultural", "فلج": "cultural", "عود": "cultural", "لبان": "cultural", "لتج": "cultural", "صفحة": "sitemap", "موقع": "sitemap", "رابط": "sitemap", "قرية": "locations", "سمائل": "locations", "مطار": "locations", "سوق": "locations", "دكان": "locations", "مرحبا": None, "هلا": None, "السلام": None, } def search_kb(query): """Return relevant knowledge-base entries as a context string.""" if not query: return "" q = query.lower() cats = set() for kw, cat in QUERY_KEYWORDS.items(): if kw in q and cat: cats.add(cat) if not cats: return "" hits = [] for cat in cats: if cat == "author": a = KB["author"] hits.append( f"Author: {a['name']} — {a['title']}. {a['bio']} " f"Philosophy: {a['philosophy']} Quote: \"{a['quote']}\"" ) elif cat in KB: for val in KB[cat].values(): hits.append(val) seen = set() unique = [] for h in hits: if h not in seen: seen.add(h) unique.append(h) return "\n---\n".join(unique[:6]) # ============================================================ # 6. CHAT FUNCTIONS # ============================================================ STATUS_READY = '
Your Voice Guide Under The Palm Tree
دليلك الصوتي تحت شجرة النخيل