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"
{get_ui_text(lang, 'no_text_selected')}
", ) 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"
{html.escape(message)}
", ) 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"
{get_ui_text(lang, 'no_text_selected')}
" safe_text = html.escape(selected_text).replace("\n", "
") safe_style = html.escape(style) return f"""
IsemPoeticVoice · {safe_style}
{safe_text}
""" 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"""

{t['title']}

{t['subtitle']}
""" # 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=""" """, ) 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( """

IsemPoeticVoice

حوّل النص العربي العادي إلى صياغات شعرية قصيرة باستخدام الذكاء الاصطناعي، ثم استمع إليها بصوت عربي طبيعي.
""" ) 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)