Spaces:
Sleeping
Sleeping
Download app.py from IsemIsCold/IsemPoeticVoice: direct link, hf CLI and curl.
- Browser
- Download file 30.9 kB
-
https://huggingface.co/spaces/IsemIsCold/IsemPoeticVoice/resolve/main/app.py
- Command line
-
hf download hf://spaces/IsemIsCold/IsemPoeticVoice/app.py
-
curl -L -o app.py https://huggingface.co/spaces/IsemIsCold/IsemPoeticVoice/resolve/main/app.py
30.9 kB
| import asyncio | |
| import html | |
| import json | |
| import os | |
| import tempfile | |
| import edge_tts | |
| import gradio as gr | |
| import requests | |
| STYLE_PROMPTS = { | |
| "رومانسي": "اكتب بصياغة شاعرية رومانسية رقيقة ومؤثرة", | |
| "حزين": "اكتب بصياغة شاعرية حزينة وعميقة ومليئة بالحنين", | |
| "فصيح كلاسيكي": "اكتب بأسلوب عربي فصيح مائل إلى النبرة الكلاسيكية والبيان الجميل", | |
| "شعر حر": "اكتب بأسلوب شعر حر حديث، مكثف، وصوري", | |
| "فخم / ملحمي": "اكتب بنبرة فخمة وقوية ومهيبة مع صور بلاغية واضحة", | |
| } | |
| # Arabic voices for Edge TTS | |
| ARABIC_VOICES = { | |
| "هديل (أنثى - السعودية)": "ar-SA-HamedNeural", | |
| "سعيد (ذكر - السعودية)": "ar-SA-HamedNeural", | |
| "نورة (أنثى - مصر)": "ar-SA-Neural", | |
| "شاكر (ذكر - مصر)": "ar-EG-ShakirNeural", | |
| "سلمى (أنثى - الإمارات)": "ar-AE-FatimaNeural", | |
| "حمد (ذكر - الإمارات)": "ar-AE-HamdanNeural", | |
| "ليلى (أنثى - العراق)": "ar-IQ-RanaNeural", | |
| "باسم (ذكر - العراق)": "ar-IQ-BasselNeural", | |
| "توفيق (ذكر - الأردن)": "ar-JO-TaimNeural", | |
| "سناء (أنثى - لبنان)": "ar-LB-LaylaNeural", | |
| "كريم (ذكر - لبنان)": "ar-LB-RamiNeural", | |
| } | |
| DEFAULT_VOICE = "ar-SA-HamedNeural" | |
| EXAMPLES = [ | |
| ["اشتقت إليها كثيرًا هذه الليلة", "رومانسي", 0.7], | |
| ["أشعر بوحدة كبيرة منذ رحيله", "حزين", 0.8], | |
| ["أريد عبارة مؤثرة عن الأمل بعد التعب", "شعر حر", 0.6], | |
| ["قلبي متعلق بها ولا أستطيع نسيانها", "فصيح كلاسيكي", 0.7], | |
| ] | |
| OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" | |
| DEFAULT_MODEL = os.getenv("OPENROUTER_MODEL", "google/gemma-3-27b-it:free") | |
| APP_URL = os.getenv("SPACE_HOST", "https://huggingface.co/spaces/IsemIsCold/IsemPoeticVoice") | |
| APP_TITLE = os.getenv("OPENROUTER_APP_TITLE", "IsemPoeticVoice") | |
| # UI Translations for multiple languages | |
| UI_TRANSLATIONS = { | |
| "العربية": { | |
| "title": "IsemPoeticVoice", | |
| "subtitle": "حوّل النص العربي العادي إلى صياغات شعرية قصيرة باستخدام الذكاء الاصطناعي، ثم استمع إليها بصوت عربي طبيعي.", | |
| "user_text_label": "النص العادي", | |
| "user_text_placeholder": "اكتب جملة عربية بسيطة مثل: اشتقت إليها كثيرًا هذه الليلة", | |
| "style_label": "الأسلوب", | |
| "creativity_label": "مستوى الإبداع", | |
| "generate_button": "توليد الصياغات الشعرية", | |
| "prompt_preview_label": "معاينة البرومبت المرسل إلى النموذج", | |
| "verse_1_label": "النص 1", | |
| "verse_2_label": "النص 2", | |
| "verse_3_label": "النص 3", | |
| "verse_choice_label": "اختر النص الذي تريد تحويله إلى صوت", | |
| "verse_choice_1": "النص 1", | |
| "verse_choice_2": "النص 2", | |
| "verse_choice_3": "النص 3", | |
| "selected_verse_label": "النص المختار", | |
| "voice_label": "الصوت", | |
| "rate_label": "سرعة الصوت", | |
| "voice_button": "توليد الصوت الشعري 🎵", | |
| "audio_output_label": "الصوت الناتج", | |
| "share_card_label": "بطاقة المعاينة", | |
| "no_text_selected": "لا يوجد نص مختار بعد.", | |
| "write_first": "اكتب جملة عربية أولًا لتوليد الأبيات.", | |
| "error_message": "حدث خطأ أثناء الاتصال بـ OpenRouter", | |
| "examples_label": "أمثلة", | |
| }, | |
| "English": { | |
| "title": "IsemPoeticVoice", | |
| "subtitle": "Transform ordinary Arabic text into short poetic verses using AI, then listen to them in natural Arabic voice.", | |
| "user_text_label": "Plain Text", | |
| "user_text_placeholder": "Write a simple Arabic sentence like: I missed her so much tonight", | |
| "style_label": "Style", | |
| "creativity_label": "Creativity Level", | |
| "generate_button": "Generate Poetic Verses", | |
| "prompt_preview_label": "Prompt Preview Sent to Model", | |
| "verse_1_label": "Verse 1", | |
| "verse_2_label": "Verse 2", | |
| "verse_3_label": "Verse 3", | |
| "verse_choice_label": "Select verse to convert to speech", | |
| "verse_choice_1": "Verse 1", | |
| "verse_choice_2": "Verse 2", | |
| "verse_choice_3": "Verse 3", | |
| "selected_verse_label": "Selected Verse", | |
| "voice_label": "Voice", | |
| "rate_label": "Speech Rate", | |
| "voice_button": "Generate Poetic Voice 🎵", | |
| "audio_output_label": "Audio Output", | |
| "share_card_label": "Preview Card", | |
| "no_text_selected": "No text selected yet.", | |
| "write_first": "Write an Arabic sentence first to generate verses.", | |
| "error_message": "An error occurred while connecting to OpenRouter", | |
| "examples_label": "Examples", | |
| }, | |
| "Français": { | |
| "title": "IsemPoeticVoice", | |
| "subtitle": "Transformez un texte arabe ordinaire en courts vers poétiques grâce à l'IA, puis écoutez-les avec une voix arabe naturelle.", | |
| "user_text_label": "Texte simple", | |
| "user_text_placeholder": "Écrivez une phrase arabe simple comme: Elle m'a beaucoup manqué ce soir", | |
| "style_label": "Style", | |
| "creativity_label": "Niveau de créativité", | |
| "generate_button": "Générer des vers poétiques", | |
| "prompt_preview_label": "Aperçu du prompt envoyé au modèle", | |
| "verse_1_label": "Vers 1", | |
| "verse_2_label": "Vers 2", | |
| "verse_3_label": "Vers 3", | |
| "verse_choice_label": "Sélectionnez le vers à convertir en parole", | |
| "verse_choice_1": "Vers 1", | |
| "verse_choice_2": "Vers 2", | |
| "verse_choice_3": "Vers 3", | |
| "selected_verse_label": "Vers sélectionné", | |
| "voice_label": "Voix", | |
| "rate_label": "Vitesse de parole", | |
| "voice_button": "Générer la voix poétique 🎵", | |
| "audio_output_label": "Sortie audio", | |
| "share_card_label": "Carte d'aperçu", | |
| "no_text_selected": "Aucun texte sélectionné pour le moment.", | |
| "write_first": "Écrivez d'abord une phrase en arabe pour générer des vers.", | |
| "error_message": "Une erreur s'est produite lors de la connexion à OpenRouter", | |
| "examples_label": "Exemples", | |
| }, | |
| "Español": { | |
| "title": "IsemPoeticVoice", | |
| "subtitle": "Transforma texto árabe ordinario en cortos versos poéticos usando IA, luego escúchalos con voz árabe natural.", | |
| "user_text_label": "Texto simple", | |
| "user_text_placeholder": "Escribe una frase árabe simple como: La extrañé mucho esta noche", | |
| "style_label": "Estilo", | |
| "creativity_label": "Nivel de creatividad", | |
| "generate_button": "Generar versos poéticos", | |
| "prompt_preview_label": "Vista previa del prompt enviado al modelo", | |
| "verse_1_label": "Verso 1", | |
| "verse_2_label": "Verso 2", | |
| "verse_3_label": "Verso 3", | |
| "verse_choice_label": "Selecciona el verso para convertir a voz", | |
| "verse_choice_1": "Verso 1", | |
| "verse_choice_2": "Verso 2", | |
| "verse_choice_3": "Verso 3", | |
| "selected_verse_label": "Verso seleccionado", | |
| "voice_label": "Voz", | |
| "rate_label": "Velocidad de habla", | |
| "voice_button": "Generar voz poética 🎵", | |
| "audio_output_label": "Salida de audio", | |
| "share_card_label": "Tarjeta de vista previa", | |
| "no_text_selected": "No hay texto seleccionado aún.", | |
| "write_first": "Escribe primero una frase en árabe para generar versos.", | |
| "error_message": "Ocurrió un error al conectar con OpenRouter", | |
| "examples_label": "Ejemplos", | |
| }, | |
| "Türkçe": { | |
| "title": "IsemPoeticVoice", | |
| "subtitle": "Sıradan Arapça metni yapay zeka kullanarak kısa şiirsel dizelere dönüştürün, ardından doğal Arapça sesle dinleyin.", | |
| "user_text_label": "Düz Metin", | |
| "user_text_placeholder": "Bu gece onu çok özledim gibi basit bir Arapça cümle yazın", | |
| "style_label": "Stil", | |
| "creativity_label": "Yaratıcılık Seviyesi", | |
| "generate_button": "Şiirsel Dizeler Oluştur", | |
| "prompt_preview_label": "Modele Gönderilen İstem Önizlemesi", | |
| "verse_1_label": "Dize 1", | |
| "verse_2_label": "Dize 2", | |
| "verse_3_label": "Dize 3", | |
| "verse_choice_label": "Sese dönüştürülecek dizeyi seçin", | |
| "verse_choice_1": "Dize 1", | |
| "verse_choice_2": "Dize 2", | |
| "verse_choice_3": "Dize 3", | |
| "selected_verse_label": "Seçilen Dize", | |
| "voice_label": "Ses", | |
| "rate_label": "Konuşma Hızı", | |
| "voice_button": "Şiirsel Ses Oluştur 🎵", | |
| "audio_output_label": "Ses Çıktısı", | |
| "share_card_label": "Önizleme Kartı", | |
| "no_text_selected": "Henüz seçili metin yok.", | |
| "write_first": "Dize oluşturmak için önce Arapça bir cümle yazın.", | |
| "error_message": "OpenRouter'a bağlanırken bir hata oluştu", | |
| "examples_label": "Örnekler", | |
| }, | |
| } | |
| # Style name translations | |
| STYLE_TRANSLATIONS = { | |
| "العربية": { | |
| "رومانسي": "رومانسي", | |
| "حزين": "حزين", | |
| "فصيح كلاسيكي": "فصيح كلاسيكي", | |
| "شعر حر": "شعر حر", | |
| "فخم / ملحمي": "فخم / ملحمي", | |
| }, | |
| "English": { | |
| "رومانسي": "Romantic", | |
| "حزين": "Sad", | |
| "فصيح كلاسيكي": "Classical", | |
| "شعر حر": "Free Verse", | |
| "فخم / ملحمي": "Epic", | |
| }, | |
| "Français": { | |
| "رومانسي": "Romantique", | |
| "حزين": "Triste", | |
| "فصيح كلاسيكي": "Classique", | |
| "شعر حر": "Vers Libre", | |
| "فخم / ملحمي": "Épique", | |
| }, | |
| "Español": { | |
| "رومانسي": "Romántico", | |
| "حزين": "Triste", | |
| "فصيح كلاسيكي": "Clásico", | |
| "شعر حر": "Verso Libre", | |
| "فخم / ملحمي": "Épico", | |
| }, | |
| "Türkçe": { | |
| "رومانسي": "Romantik", | |
| "حزين": "Hüzünlü", | |
| "فصيح كلاسيكي": "Klasik", | |
| "شعر حر": "Serbest Nazım", | |
| "فخم / ملحمي": "Destansı", | |
| }, | |
| } | |
| def get_ui_text(lang: str, key: str) -> str: | |
| """Get UI text for a given language and key""" | |
| return UI_TRANSLATIONS.get(lang, UI_TRANSLATIONS["العربية"]).get(key, key) | |
| def get_style_choices(lang: str) -> list: | |
| """Get style choices with translated display names""" | |
| style_trans = STYLE_TRANSLATIONS.get(lang, STYLE_TRANSLATIONS["العربية"]) | |
| return [f"{trans} ({arabic})" for arabic, trans in style_trans.items()] | |
| def get_style_from_choice(choice: str) -> str: | |
| """Extract Arabic style name from choice string""" | |
| # Format is "Translation (العربية)" | |
| if "(" in choice and ")" in choice: | |
| start = choice.rfind("(") + 1 | |
| end = choice.rfind(")") | |
| return choice[start:end] | |
| return choice | |
| def build_prompt(user_text: str, style: str, creativity: float) -> str: | |
| style_instruction = STYLE_PROMPTS.get(style, STYLE_PROMPTS["شعر حر"]) | |
| return ( | |
| "حوّل النص العربي العادي الآتي إلى 3 صيغ شعرية قصيرة وجميلة.\n" | |
| f"{style_instruction}.\n" | |
| f"مستوى الإبداع المطلوب: {creativity:.1f} من 1.0.\n" | |
| "الشروط:\n" | |
| "- حافظ على المعنى الأساسي.\n" | |
| "- اجعل المخرجات مناسبة للإلقاء الصوتي.\n" | |
| "- لا تكتب أي شرح خارج JSON.\n" | |
| "- استخدم العربية الفصحى السلسة.\n" | |
| "- اجعل كل صيغة مختلفة عن الأخرى بوضوح.\n\n" | |
| f"النص الأصلي: {user_text.strip()}" | |
| ) | |
| def call_openrouter(user_text: str, style: str, creativity: float): | |
| api_key = ( | |
| os.getenv("OPENROUTER_API_KEY") | |
| or os.getenv("OPENROUTERAPIKEY") | |
| or os.getenv("OPEN_ROUTER_API_KEY") | |
| ) | |
| if not api_key: | |
| all_env_names = sorted( | |
| [key for key in os.environ.keys() if "OPENROUTER" in key or "ROUTER" in key] | |
| ) | |
| debug_names = ", ".join(all_env_names) if all_env_names else "No related env vars found" | |
| raise RuntimeError( | |
| "Missing OpenRouter API key. Expected one of: OPENROUTER_API_KEY, " | |
| f"OPENROUTERAPIKEY, OPEN_ROUTER_API_KEY. Detected env names: {debug_names}" | |
| ) | |
| prompt = build_prompt(user_text, style, creativity) | |
| system_message = ( | |
| "أنت مساعد متخصص في إعادة كتابة النصوص العربية العادية إلى صياغات شعرية عربية جميلة. " | |
| "أعد النتيجة فقط بصيغة JSON صحيحة وبدون أي شرح إضافي. " | |
| 'صيغة JSON المطلوبة هي: {"variants":["النص الأول","النص الثاني","النص الثالث"]}. ' | |
| "يجب أن تحتوي variants على 3 نصوص شعرية قصيرة فقط." | |
| ) | |
| payload = { | |
| "model": DEFAULT_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": system_message}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "temperature": creativity, | |
| "max_tokens": 350, | |
| "response_format": {"type": "json_object"}, | |
| } | |
| headers = { | |
| "Authorization": f"Bearer {api_key}", | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": APP_URL, | |
| "X-OpenRouter-Title": APP_TITLE, | |
| } | |
| response = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=60) | |
| # Handle rate limiting (429) more gracefully | |
| if response.status_code == 429: | |
| raise RuntimeError("Rate limit exceeded. Please wait a moment and try again, or add credits to your OpenRouter account.") | |
| response.raise_for_status() | |
| data = response.json() | |
| content = data["choices"][0]["message"]["content"] | |
| parsed = json.loads(content) | |
| variants = parsed.get("variants", []) | |
| if not isinstance(variants, list) or len(variants) < 3: | |
| raise RuntimeError("OpenRouter returned an unexpected format. Expected 3 variants in JSON.") | |
| cleaned = [str(item).strip() for item in variants[:3]] | |
| while len(cleaned) < 3: | |
| cleaned.append("") | |
| return prompt, cleaned | |
| def generate_poetry(user_text: str, style: str, creativity: float, lang: str = "العربية"): | |
| user_text = user_text.strip() | |
| if not user_text: | |
| empty = "" | |
| return ( | |
| get_ui_text(lang, "write_first"), | |
| empty, | |
| empty, | |
| empty, | |
| empty, | |
| None, | |
| f"<div dir='rtl'>{get_ui_text(lang, 'no_text_selected')}</div>", | |
| ) | |
| try: | |
| prompt_preview, variants = call_openrouter(user_text, style, creativity) | |
| selected = variants[0] | |
| share_card = make_share_card(selected, style, lang) | |
| return ( | |
| prompt_preview, | |
| variants[0], | |
| variants[1], | |
| variants[2], | |
| selected, | |
| None, | |
| share_card, | |
| ) | |
| except Exception as error: | |
| message = f"{get_ui_text(lang, 'error_message')}: {error}" | |
| return ( | |
| message, | |
| "", | |
| "", | |
| "", | |
| "", | |
| None, | |
| f"<div dir='rtl'>{html.escape(message)}</div>", | |
| ) | |
| def select_variant(choice: str, verse_1: str, verse_2: str, verse_3: str) -> str: | |
| mapping = { | |
| "النص 1": verse_1, | |
| "النص 2": verse_2, | |
| "النص 3": verse_3, | |
| } | |
| return mapping.get(choice, verse_1) | |
| def run_async(coro): | |
| """Helper to run async functions in sync context""" | |
| return asyncio.run(coro) | |
| async def generate_speech_async(text: str, voice: str = DEFAULT_VOICE, rate: int = 0): | |
| """Generate speech from text using Edge TTS""" | |
| if not text or not text.strip(): | |
| return None | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: | |
| output_path = temp_file.name | |
| try: | |
| communicate = edge_tts.Communicate(text, voice, rate=f"{rate:+d}%" if rate != 0 else "+0%") | |
| await communicate.save(output_path) | |
| return output_path | |
| except Exception as e: | |
| print(f"Error generating speech: {e}") | |
| return None | |
| def generate_speech(text: str, voice: str = DEFAULT_VOICE, rate: int = 0): | |
| """Synchronous wrapper for generate_speech_async""" | |
| return run_async(generate_speech_async(text, voice, rate)) | |
| def make_share_card(selected_text: str, style: str, lang: str = "العربية") -> str: | |
| if not selected_text.strip(): | |
| return f"<div dir='rtl'>{get_ui_text(lang, 'no_text_selected')}</div>" | |
| safe_text = html.escape(selected_text).replace("\n", "<br>") | |
| safe_style = html.escape(style) | |
| return f""" | |
| <div dir="rtl" style=" | |
| border: 1px solid #d4d4d8; | |
| border-radius: 16px; | |
| padding: 18px; | |
| background: linear-gradient(135deg, #fcfcff, #f3f4f6); | |
| color: #111827; | |
| line-height: 1.9; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.06); | |
| font-family: Arial, sans-serif; | |
| "> | |
| <div style="font-size: 14px; color: #6b7280; margin-bottom: 10px;"> | |
| IsemPoeticVoice · {safe_style} | |
| </div> | |
| <div style="font-size: 24px; white-space: normal;">{safe_text}</div> | |
| </div> | |
| """ | |
| def update_ui_for_language(lang): | |
| """Update all UI elements for the selected language""" | |
| t = UI_TRANSLATIONS.get(lang, UI_TRANSLATIONS["العربية"]) | |
| # Update title and subtitle | |
| title_html = f""" | |
| <div class="app-title"> | |
| <h1>{t['title']}</h1> | |
| </div> | |
| <div class="app-subtitle"> | |
| {t['subtitle']} | |
| </div> | |
| """ | |
| # Get style choices for this language | |
| style_choices = get_style_choices(lang) | |
| # Update verse choice options | |
| verse_choices = [t['verse_choice_1'], t['verse_choice_2'], t['verse_choice_3']] | |
| return ( | |
| # Title | |
| title_html, | |
| # User text | |
| gr.update(label=t['user_text_label'], placeholder=t['user_text_placeholder']), | |
| # Style | |
| gr.update(label=t['style_label'], choices=style_choices, value=style_choices[0]), | |
| # Creativity | |
| gr.update(label=t['creativity_label']), | |
| # Generate button | |
| gr.update(value=t['generate_button']), | |
| # Prompt preview | |
| gr.update(label=t['prompt_preview_label']), | |
| # Verse labels | |
| gr.update(label=t['verse_1_label']), | |
| gr.update(label=t['verse_2_label']), | |
| gr.update(label=t['verse_3_label']), | |
| # Verse choice | |
| gr.update(label=t['verse_choice_label'], choices=verse_choices, value=verse_choices[0]), | |
| # Selected verse | |
| gr.update(label=t['selected_verse_label']), | |
| # Voice | |
| gr.update(label=t['voice_label']), | |
| # Rate | |
| gr.update(label=t['rate_label']), | |
| # Voice button | |
| gr.update(value=t['voice_button']), | |
| # Audio output | |
| gr.update(label=t['audio_output_label']), | |
| # Share card | |
| gr.update(label=t['share_card_label']), | |
| ) | |
| # Theme CSS overrides for custom themes | |
| THEME_CSS_OVERRIDES = { | |
| "Soft": "", # Default, no overrides needed | |
| "Default": """ | |
| :root { --primary-500: #1f2937; --primary-600: #111827; } | |
| .gradio-container { background-color: #f9fafb; } | |
| """, | |
| "Monochrome": """ | |
| :root { | |
| --primary-50: #000000; --primary-100: #171717; --primary-200: #262626; | |
| --primary-300: #404040; --primary-400: #525252; --primary-500: #737373; | |
| --primary-600: #a3a3a3; --primary-700: #d4d4d4; --primary-800: #e5e5e5; | |
| --primary-900: #f0f0f0; --primary-950: #fafafa; | |
| --body-background-fill: #0a0a0a; | |
| --block-background-fill: #171717; | |
| --background-fill-primary: #0a0a0a; | |
| --background-fill-secondary: #171717; | |
| } | |
| .gradio-container { background-color: #0a0a0a !important; color: #e5e5e5 !important; } | |
| .prose, h1, h2, h3, h4, h5, h6, p, span, label { color: #e5e5e5 !important; } | |
| .gr-button-primary { background: #737373 !important; color: #000 !important; } | |
| """, | |
| "Base": """ | |
| :root { | |
| --primary-500: #3b82f6; --primary-600: #2563eb; | |
| --body-background-fill: #ffffff; | |
| --block-background-fill: #f3f4f6; | |
| } | |
| .gradio-container { background-color: #ffffff !important; } | |
| .gr-box { border: 1px solid #e5e7eb !important; } | |
| """, | |
| "Glass": """ | |
| :root { | |
| --primary-500: #8b5cf6; --primary-600: #7c3aed; | |
| --body-background-fill: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .gradio-container { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| min-height: 100vh; | |
| } | |
| .gr-box, .gr-pandas, .gr-plot { | |
| background: rgba(255, 255, 255, 0.15) !important; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| } | |
| .prose, h1, h2, h3, h4, h5, h6, p, span, label { color: #ffffff !important; } | |
| .gr-button-primary { | |
| background: rgba(255, 255, 255, 0.3) !important; | |
| backdrop-filter: blur(5px); | |
| color: #fff !important; | |
| border: 1px solid rgba(255, 255, 255, 0.4) !important; | |
| } | |
| """, | |
| } | |
| # Get initial theme from environment or localStorage (via JS) | |
| INITIAL_THEME = os.getenv("GRADIO_THEME", "Soft") | |
| # Build initial CSS with theme override | |
| INITIAL_CSS = """ | |
| .app-title { text-align: center; direction: rtl; } | |
| .app-subtitle { text-align: center; direction: rtl; color: #4b5563; } | |
| .rtl textarea, .rtl input { direction: rtl !important; text-align: right !important; } | |
| """ + THEME_CSS_OVERRIDES.get(INITIAL_THEME, "") | |
| with gr.Blocks( | |
| title="IsemPoeticVoice", | |
| theme=gr.themes.Soft(), | |
| css=INITIAL_CSS, | |
| head=""" | |
| <script> | |
| // Apply saved theme from localStorage on page load | |
| (function() { | |
| var savedTheme = localStorage.getItem('gradio_theme') || 'Soft'; | |
| var themeStyles = { | |
| 'Soft': '', | |
| 'Default': ':root { --primary-500: #1f2937; --primary-600: #111827; } .gradio-container { background-color: #f9fafb; }', | |
| 'Monochrome': ':root { --primary-500: #737373; --body-background-fill: #0a0a0a; --block-background-fill: #171717; } .gradio-container { background-color: #0a0a0a !important; color: #e5e5e5 !important; } .prose, h1, h2, h3, h4, h5, h6, p, span, label { color: #e5e5e5 !important; } .gr-button-primary { background: #737373 !important; color: #000 !important; }', | |
| 'Base': ':root { --primary-500: #3b82f6; --primary-600: #2563eb; } .gradio-container { background-color: #ffffff !important; } .gr-box { border: 1px solid #e5e7eb !important; }', | |
| 'Glass': ':root { --primary-500: #8b5cf6; --primary-600: #7c3aed; } .gradio-container { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; } .gr-box { background: rgba(255,255,255,0.15) !important; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2) !important; } .prose, h1, h2, h3, h4, h5, h6, p, span, label { color: #ffffff !important; } .gr-button-primary { background: rgba(255,255,255,0.3) !important; color: #fff !important; border: 1px solid rgba(255,255,255,0.4) !important; }' | |
| }; | |
| if (savedTheme && themeStyles[savedTheme]) { | |
| var style = document.createElement('style'); | |
| style.id = 'theme-override-style'; | |
| style.textContent = themeStyles[savedTheme]; | |
| document.head.appendChild(style); | |
| } | |
| // Set the dropdown value to saved theme | |
| document.addEventListener('DOMContentLoaded', function() { | |
| var dropdowns = document.querySelectorAll('[aria-label="Theme / المظهر"]'); | |
| dropdowns.forEach(function(d) { | |
| if (d.tagName === 'SELECT') { | |
| d.value = savedTheme; | |
| } | |
| }); | |
| }); | |
| })(); | |
| </script> | |
| """, | |
| ) as demo: | |
| # Language and theme selectors | |
| with gr.Row(): | |
| lang_selector = gr.Dropdown( | |
| label="Language / اللغة", | |
| choices=["العربية", "English", "Français", "Español", "Türkçe"], | |
| value="العربية", | |
| interactive=True, | |
| scale=1, | |
| ) | |
| theme_selector = gr.Dropdown( | |
| label="Theme / المظهر", | |
| choices=["Soft", "Default", "Monochrome", "Base", "Glass"], | |
| value="Soft", | |
| interactive=True, | |
| scale=1, | |
| ) | |
| # Title and subtitle | |
| title_md = gr.Markdown( | |
| """ | |
| <div class="app-title"> | |
| <h1>IsemPoeticVoice</h1> | |
| </div> | |
| <div class="app-subtitle"> | |
| حوّل النص العربي العادي إلى صياغات شعرية قصيرة باستخدام الذكاء الاصطناعي، ثم استمع إليها بصوت عربي طبيعي. | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| user_text = gr.Textbox( | |
| label="النص العادي", | |
| placeholder="اكتب جملة عربية بسيطة مثل: اشتقت إليها كثيرًا هذه الليلة", | |
| lines=5, | |
| elem_classes=["rtl"], | |
| ) | |
| style = gr.Dropdown( | |
| label="الأسلوب", | |
| choices=["رومانسي (رومانسي)", "حزين (حزين)", "فصيح كلاسيكي (فصيح كلاسيكي)", "شعر حر (شعر حر)", "فخم / ملحمي (فخم / ملحمي)"], | |
| value="رومانسي (رومانسي)", | |
| ) | |
| creativity = gr.Slider( | |
| label="مستوى الإبداع", | |
| minimum=0.1, | |
| maximum=1.0, | |
| value=0.7, | |
| step=0.1, | |
| ) | |
| generate_button = gr.Button("توليد الصياغات الشعرية", variant="primary") | |
| prompt_preview = gr.Textbox( | |
| label="معاينة البرومبت المرسل إلى النموذج", | |
| lines=10, | |
| interactive=False, | |
| elem_classes=["rtl"], | |
| ) | |
| with gr.Column(scale=1): | |
| verse_1 = gr.Textbox(label="النص 1", lines=3, interactive=False, elem_classes=["rtl"]) | |
| verse_2 = gr.Textbox(label="النص 2", lines=3, interactive=False, elem_classes=["rtl"]) | |
| verse_3 = gr.Textbox(label="النص 3", lines=3, interactive=False, elem_classes=["rtl"]) | |
| verse_choice = gr.Radio( | |
| label="اختر النص الذي تريد تحويله إلى صوت", | |
| choices=["النص 1", "النص 2", "النص 3"], | |
| value="النص 1", | |
| ) | |
| selected_verse = gr.Textbox( | |
| label="النص المختار", | |
| lines=4, | |
| interactive=False, | |
| elem_classes=["rtl"], | |
| ) | |
| # Custom text for direct voice generation | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| custom_text = gr.Textbox( | |
| label="نص مخصص للتحويل إلى صوت", | |
| placeholder="اكتب أي نص عربي هنا لتحويله مباشرة إلى صوت...", | |
| lines=3, | |
| elem_classes=["rtl"], | |
| ) | |
| with gr.Column(scale=1): | |
| voice_dropdown = gr.Dropdown( | |
| label="الصوت", | |
| choices=list(ARABIC_VOICES.keys()), | |
| value=list(ARABIC_VOICES.keys())[0], | |
| ) | |
| rate_slider = gr.Slider( | |
| label="سرعة الصوت", | |
| minimum=-50, | |
| maximum=50, | |
| value=0, | |
| step=10, | |
| ) | |
| voice_button = gr.Button("توليد الصوت 🎵", variant="primary") | |
| audio_output = gr.Audio(label="الصوت الناتج", type="filepath") | |
| share_card = gr.HTML(label="بطاقة المعاينة") | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=[user_text, style, creativity], | |
| ) | |
| # Update UI when language changes | |
| lang_selector.change( | |
| fn=update_ui_for_language, | |
| inputs=[lang_selector], | |
| outputs=[ | |
| title_md, | |
| user_text, | |
| style, | |
| creativity, | |
| generate_button, | |
| prompt_preview, | |
| verse_1, | |
| verse_2, | |
| verse_3, | |
| verse_choice, | |
| selected_verse, | |
| voice_dropdown, | |
| rate_slider, | |
| voice_button, | |
| audio_output, | |
| share_card, | |
| ], | |
| ) | |
| # Update theme when theme selector changes | |
| theme_selector.change( | |
| fn=None, | |
| inputs=[theme_selector], | |
| outputs=None, | |
| js=""" | |
| function(theme) { | |
| localStorage.setItem('gradio_theme', theme); | |
| location.reload(); | |
| } | |
| """, | |
| ) | |
| generate_button.click( | |
| fn=generate_poetry, | |
| inputs=[user_text, style, creativity], | |
| outputs=[prompt_preview, verse_1, verse_2, verse_3, selected_verse, audio_output, share_card], | |
| ) | |
| verse_choice.change( | |
| fn=select_variant, | |
| inputs=[verse_choice, verse_1, verse_2, verse_3], | |
| outputs=selected_verse, | |
| ) | |
| # Voice generation - uses custom text if provided, otherwise uses selected verse | |
| def generate_voice_with_fallback(custom_text_val, selected_verse_val, voice_name, rate): | |
| """Use custom text if provided, otherwise use selected verse""" | |
| text_to_use = custom_text_val.strip() if custom_text_val and custom_text_val.strip() else selected_verse_val | |
| return generate_speech(text_to_use, ARABIC_VOICES[voice_name], rate) | |
| voice_button.click( | |
| fn=generate_voice_with_fallback, | |
| inputs=[custom_text, selected_verse, voice_dropdown, rate_slider], | |
| outputs=audio_output, | |
| ) | |
| selected_verse.change( | |
| fn=make_share_card, | |
| inputs=[selected_verse, style], | |
| outputs=share_card, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |