IsemPoeticVoice / app.py
IsemIsCold's picture
Add custom text input for direct voice generation
c50ed44
Raw History Blame Contribute Delete
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)