Update app.py
Browse files
app.py
CHANGED
|
@@ -6,40 +6,87 @@ import os
|
|
| 6 |
from huggingface_hub import HfApi
|
| 7 |
from datetime import datetime
|
| 8 |
|
| 9 |
-
# إعدادات الأمان
|
| 10 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 11 |
REPO_ID = "Alide21/speech_quran"
|
| 12 |
api = HfApi()
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
custom_css = """
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
background: white !important;
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
-
.ayah-
|
| 25 |
-
|
| 26 |
-
border-radius: 15px !important;
|
| 27 |
-
padding: 40px 20px !important;
|
| 28 |
-
font-size: 30px !important;
|
| 29 |
-
color: #2c3e50 !important;
|
| 30 |
line-height: 2 !important;
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
}
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
|
|
|
|
|
|
|
|
|
| 43 |
"""
|
| 44 |
|
| 45 |
def clean_text(text):
|
|
@@ -50,125 +97,146 @@ def load_quran_data():
|
|
| 50 |
for s in range(1, 115):
|
| 51 |
try:
|
| 52 |
url = f"https://raw.githubusercontent.com/CheeseWithSauce/TheHolyQuranJSONFormat/refs/heads/main/tajweedsurahs/{s:03}.json"
|
| 53 |
-
res = requests.get(url, timeout=10)
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
for v in data["verses"]:
|
| 58 |
-
txt = clean_text(v.get("text_ar", ""))
|
| 59 |
all_verses.append({
|
| 60 |
-
"
|
| 61 |
"surah": s,
|
| 62 |
"ayah": v["ayah"],
|
| 63 |
"text": txt,
|
| 64 |
-
"surah_name":
|
| 65 |
})
|
| 66 |
except: continue
|
| 67 |
return all_verses
|
| 68 |
|
| 69 |
ALL_AYAT = load_quran_data()
|
| 70 |
|
| 71 |
-
def
|
| 72 |
try:
|
| 73 |
files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
|
| 74 |
return len([f for f in files if f.startswith("data/")])
|
| 75 |
except: return 0
|
| 76 |
|
| 77 |
-
def
|
| 78 |
-
if
|
| 79 |
-
return "⚠️ يرجى تسجيل الصوت أولاً قبل الإرسال", gr.update(), gr.update(), current_idx, session_count, get_stats()
|
| 80 |
|
| 81 |
-
|
| 82 |
-
ext =
|
| 83 |
-
|
| 84 |
-
file_name = f"{verse['surah']}|{verse['ayah']}.{ext}"
|
| 85 |
|
| 86 |
try:
|
| 87 |
-
api.upload_file(
|
| 88 |
-
path_or_fileobj=audio_path,
|
| 89 |
-
path_in_repo=f"data/{file_name}",
|
| 90 |
-
repo_id=REPO_ID,
|
| 91 |
-
repo_type="dataset",
|
| 92 |
-
token=HF_TOKEN
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# الانتقال للآية التالية تلقائياً مع فحص الطول
|
| 96 |
-
next_idx = current_idx + 1
|
| 97 |
-
while next_idx < len(ALL_AYAT) and len(ALL_AYAT[next_idx]["text"]) > 200:
|
| 98 |
-
next_idx += 1
|
| 99 |
-
|
| 100 |
-
if next_idx >= len(ALL_AYAT): next_idx = 0
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
|
| 106 |
-
|
| 107 |
-
return (
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
except Exception as e:
|
| 111 |
-
return f"
|
| 112 |
|
| 113 |
-
def
|
|
|
|
| 114 |
for i, v in enumerate(ALL_AYAT):
|
| 115 |
-
if v["surah"] ==
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
with gr.Blocks(title="خادم القرآن") as demo:
|
| 123 |
-
# تخزين المتغيرات برمجياً
|
| 124 |
-
current_verse_idx = gr.State(value=random.randint(0, len(ALL_AYAT)-1) if ALL_AYAT else 0)
|
| 125 |
-
session_counter = gr.State(value=0)
|
| 126 |
|
| 127 |
-
with gr.Column(elem_classes="main-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
if ALL_AYAT:
|
| 136 |
-
init_idx = current_verse_idx.value
|
| 137 |
-
initial_v = ALL_AYAT[init_idx]
|
| 138 |
-
ayah_info = gr.Markdown(f"### سورة {initial_v['surah_name']} - آية {initial_v['ayah']}")
|
| 139 |
-
ayah_display = gr.HTML(f"<div class='ayah-box'>{initial_v['text']}</div>")
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
|
|
|
|
| 150 |
with gr.Row():
|
| 151 |
-
|
| 152 |
-
a_input = gr.Number(label="رقم الآية", precision=0)
|
| 153 |
go_btn = gr.Button("انتقال")
|
| 154 |
|
| 155 |
-
#
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
idx = random.randint(0, len(ALL_AYAT)-1)
|
| 161 |
-
v = ALL_AYAT[idx]
|
| 162 |
-
return f"<div class='ayah-box'>{v['text']}</div>", f"### سورة {v['surah_name']} - آية {v['ayah']}", idx
|
| 163 |
-
|
| 164 |
-
random_btn.click(get_random, outputs=[ayah_display, ayah_info, current_verse_idx])
|
| 165 |
-
go_btn.click(jump_to_verse, inputs=[s_input, a_input], outputs=[ayah_display, ayah_info, current_verse_idx])
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
)
|
| 172 |
|
| 173 |
-
#
|
| 174 |
-
demo.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from huggingface_hub import HfApi
|
| 7 |
from datetime import datetime
|
| 8 |
|
| 9 |
+
# إعدادات الأمان
|
| 10 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 11 |
REPO_ID = "Alide21/speech_quran"
|
| 12 |
api = HfApi()
|
| 13 |
|
| 14 |
+
# قائمة أسماء السور للتحكم السهل
|
| 15 |
+
SURAH_NAMES = [
|
| 16 |
+
"الفاتحة", "البقرة", "آل عمران", "النساء", "المائدة", "الأنعام", "الأعراف", "الأنفال", "التوبة", "يونس", "هود", "يوسف", "الرعد", "إبراهيم", "الحجر", "النحل", "الإسراء", "الكهف", "مريم", "طه", "الأنبياء", "الحج", "المؤمنون", "النور", "الفرقان", "الشعراء", "النمل", "القصص", "العنكبوت", "الروم", "لقمان", "السجدة", "الأحزاب", "سبأ", "فاطر", "يس", "الصافات", "ص", "الزمر", "غافر", "فصلت", "الشورى", "الزخرف", "الدخان", "الجاثية", "الأحقاف", "محمد", "الفتح", "الحجرات", "ق", "الذاريات", "الطور", "النجم", "القمر", "الرحمن", "الواقعة", "الحديد", "المجادلة", "الحشر", "الممتحنة", "الصف", "الجمعة", "المنافقون", "التغابن", "الطلاق", "التحريم", "الملك", "القلم", "الحاقة", "المعارج", "نوح", "الجن", "المزمل", "المدثر", "القيامة", "الإنسان", "المرسلات", "النبأ", "النازعات", "عبس", "التكوير", "الانفطار", "المطففين", "الانشقاق", "البروج", "الطارق", "الأعلى", "الغاشية", "الفجر", "البلد", "الشمس", "الليل", "الضحى", "الشرح", "التين", "العلق", "القدر", "البينة", "الزلزلة", "العاديات", "القارعة", "التكاثر", "العصر", "الهمزة", "الفيل", "قريش", "الماعون", "الكوثر", "الكافرون", "النصر", "المسد", "الإخلاص", "الفلق", "الناس"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
# CSS احترافي جداً لتحويل الواجهة
|
| 20 |
custom_css = """
|
| 21 |
+
@import url('https://fonts.googleapis.com/css2?family=Amiri:wght@400;700&family=Reem+Kufi:wght@400;700&display=swap');
|
| 22 |
+
|
| 23 |
+
.gradio-container {
|
| 24 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
|
| 25 |
+
direction: rtl !important;
|
| 26 |
+
}
|
| 27 |
+
.main-wrapper {
|
| 28 |
+
max-width: 900px !important;
|
| 29 |
+
margin: auto !important;
|
| 30 |
+
font-family: 'Amiri', serif !important;
|
| 31 |
+
}
|
| 32 |
+
.header-section {
|
| 33 |
+
text-align: center;
|
| 34 |
+
padding: 40px 0;
|
| 35 |
+
color: #2c3e50;
|
| 36 |
+
font-family: 'Reem Kufi', sans-serif;
|
| 37 |
+
}
|
| 38 |
+
.stats-container {
|
| 39 |
+
display: flex;
|
| 40 |
+
justify-content: center;
|
| 41 |
+
gap: 20px;
|
| 42 |
+
margin-bottom: 30px;
|
| 43 |
+
}
|
| 44 |
+
.stat-card {
|
| 45 |
+
background: white;
|
| 46 |
+
padding: 15px 25px;
|
| 47 |
+
border-radius: 15px;
|
| 48 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
| 49 |
+
text-align: center;
|
| 50 |
+
border-top: 4px solid #d4af37;
|
| 51 |
+
min-width: 150px;
|
| 52 |
+
animation: slideUp 0.8s ease-out;
|
| 53 |
+
}
|
| 54 |
+
.stat-value { font-size: 24px; font-weight: bold; color: #d4af37; }
|
| 55 |
+
.stat-label { font-size: 14px; color: #7f8c8d; }
|
| 56 |
+
|
| 57 |
+
.ayah-card {
|
| 58 |
background: white !important;
|
| 59 |
+
border-radius: 25px !important;
|
| 60 |
+
padding: 50px 30px !important;
|
| 61 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.1) !important;
|
| 62 |
+
border: 1px solid #eee !important;
|
| 63 |
+
text-align: center !important;
|
| 64 |
}
|
| 65 |
+
.ayah-text {
|
| 66 |
+
font-size: 36px !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
line-height: 2 !important;
|
| 68 |
+
color: #2c3e50 !important;
|
| 69 |
+
margin-bottom: 30px !important;
|
| 70 |
+
padding: 20px;
|
| 71 |
}
|
| 72 |
+
.surah-badge {
|
| 73 |
+
display: inline-block;
|
| 74 |
+
background: #fdf6e3;
|
| 75 |
+
color: #b8860b;
|
| 76 |
+
padding: 5px 20px;
|
| 77 |
+
border-radius: 50px;
|
| 78 |
+
font-weight: bold;
|
| 79 |
+
margin-bottom: 20px;
|
| 80 |
+
border: 1px solid #d4af37;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
@keyframes slideUp {
|
| 84 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 85 |
+
to { opacity: 1; transform: translateY(0); }
|
| 86 |
}
|
| 87 |
+
|
| 88 |
+
/* تعديل RTL للأزرار والمدخلات */
|
| 89 |
+
.rtl-fixed { direction: rtl !important; text-align: right !important; }
|
| 90 |
"""
|
| 91 |
|
| 92 |
def clean_text(text):
|
|
|
|
| 97 |
for s in range(1, 115):
|
| 98 |
try:
|
| 99 |
url = f"https://raw.githubusercontent.com/CheeseWithSauce/TheHolyQuranJSONFormat/refs/heads/main/tajweedsurahs/{s:03}.json"
|
| 100 |
+
res = requests.get(url, timeout=10).json()
|
| 101 |
+
for v in res["verses"]:
|
| 102 |
+
txt = clean_text(v.get("text_ar", ""))
|
| 103 |
+
if 0 < len(txt) < 200:
|
|
|
|
|
|
|
| 104 |
all_verses.append({
|
| 105 |
+
"id": len(all_verses),
|
| 106 |
"surah": s,
|
| 107 |
"ayah": v["ayah"],
|
| 108 |
"text": txt,
|
| 109 |
+
"surah_name": SURAH_NAMES[s-1]
|
| 110 |
})
|
| 111 |
except: continue
|
| 112 |
return all_verses
|
| 113 |
|
| 114 |
ALL_AYAT = load_quran_data()
|
| 115 |
|
| 116 |
+
def get_total_count():
|
| 117 |
try:
|
| 118 |
files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
|
| 119 |
return len([f for f in files if f.startswith("data/")])
|
| 120 |
except: return 0
|
| 121 |
|
| 122 |
+
def on_submit(audio, current_idx, session_count):
|
| 123 |
+
if not audio: return "الرجاء التسجيل أولاً", gr.update(), gr.update(), current_idx, session_count, get_total_count()
|
|
|
|
| 124 |
|
| 125 |
+
v = ALL_AYAT[current_idx]
|
| 126 |
+
ext = audio.split('.')[-1]
|
| 127 |
+
file_name = f"{v['surah']}|{v['ayah']}.{ext}"
|
|
|
|
| 128 |
|
| 129 |
try:
|
| 130 |
+
api.upload_file(path_or_fileobj=audio, path_in_repo=f"data/{file_name}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
+
# الانتقال للآية التالية
|
| 133 |
+
new_idx = (current_idx + 1) % len(ALL_AYAT)
|
| 134 |
+
while len(ALL_AYAT[new_idx]["text"]) > 200: new_idx = (new_idx + 1) % len(ALL_AYAT)
|
| 135 |
|
| 136 |
+
nv = ALL_AYAT[new_idx]
|
| 137 |
+
return (
|
| 138 |
+
"✅ تم الحفظ بنجاح!",
|
| 139 |
+
f"<div class='ayah-box ayah-text'>{nv['text']}</div>",
|
| 140 |
+
f"سورة {nv['surah_name']} - آية {nv['ayah']}",
|
| 141 |
+
new_idx, session_count + 1, get_total_count()
|
| 142 |
+
)
|
| 143 |
except Exception as e:
|
| 144 |
+
return f"خطأ: {str(e)}", gr.update(), gr.update(), current_idx, session_count, get_total_count()
|
| 145 |
|
| 146 |
+
def select_specific(surah_name, ayah_num):
|
| 147 |
+
s_idx = SURAH_NAMES.index(surah_name) + 1
|
| 148 |
for i, v in enumerate(ALL_AYAT):
|
| 149 |
+
if v["surah"] == s_idx and v["ayah"] == ayah_num:
|
| 150 |
+
return f"<div class='ayah-box ayah-text'>{v['text']}</div>", f"سورة {v['surah_name']} - آية {v['ayah']}", i
|
| 151 |
+
return "الآية غير موجودة أو طويلة جداً", "خطأ", 0
|
| 152 |
+
|
| 153 |
+
with gr.Blocks(css=custom_css) as demo:
|
| 154 |
+
idx_state = gr.State(random.randint(0, len(ALL_AYAT)-1))
|
| 155 |
+
sess_state = gr.State(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
with gr.Column(elem_classes="main-wrapper"):
|
| 158 |
+
# Header
|
| 159 |
+
gr.HTML("""
|
| 160 |
+
<div class='header-section'>
|
| 161 |
+
<h1>🌙 منصة تلاوة</h1>
|
| 162 |
+
<p>ساهم في بناء مستقبل الذكاء الاصطناعي لخدمة القرآن الكريم</p>
|
| 163 |
+
</div>
|
| 164 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
+
# Stats Cards
|
| 167 |
+
with gr.HTML():
|
| 168 |
+
total = get_total_count()
|
| 169 |
+
gr.HTML(f"""
|
| 170 |
+
<div class='stats-container'>
|
| 171 |
+
<div class='stat-card'>
|
| 172 |
+
<div class='stat-label'>إجمالي التسجيلات</div>
|
| 173 |
+
<div class='stat-value'>{total}</div>
|
| 174 |
+
</div>
|
| 175 |
+
<div id='sess_card' class='stat-card'>
|
| 176 |
+
<div class='stat-label'>مساهماتك الآن</div>
|
| 177 |
+
<div class='stat-value'>0</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
""")
|
| 181 |
+
|
| 182 |
+
# Ayah Display Card
|
| 183 |
+
with gr.Column(elem_classes="ayah-card"):
|
| 184 |
+
init_v = ALL_AYAT[idx_state.value]
|
| 185 |
+
info_html = gr.HTML(f"<div class='surah-badge'>سورة {init_v['surah_name']} - آية {init_v['ayah']}</div>")
|
| 186 |
+
text_html = gr.HTML(f"<div class='ayah-box ayah-text'>{init_v['text']}</div>")
|
| 187 |
+
|
| 188 |
+
audio_ui = gr.Audio(sources=["microphone", "upload"], type="filepath", label="سجل تلاوتك الآن")
|
| 189 |
+
|
| 190 |
+
with gr.Row():
|
| 191 |
+
send_btn = gr.Button("إرسال التسجيل ✅", variant="primary", scale=2)
|
| 192 |
+
rnd_btn = gr.Button("آية عشوائية 🎲", scale=1)
|
| 193 |
|
| 194 |
+
# Controls
|
| 195 |
+
with gr.Accordion("⚙️ خيارات متقدمة", open=False):
|
| 196 |
with gr.Row():
|
| 197 |
+
s_dropdown = gr.Dropdown(choices=SURAH_NAMES, label="اختر السورة", value="الفاتحة")
|
| 198 |
+
a_input = gr.Number(label="رقم الآية", value=1, precision=0)
|
| 199 |
go_btn = gr.Button("انتقال")
|
| 200 |
|
| 201 |
+
# Functions
|
| 202 |
+
def get_rnd():
|
| 203 |
+
ni = random.randint(0, len(ALL_AYAT)-1)
|
| 204 |
+
v = ALL_AYAT[ni]
|
| 205 |
+
return f"<div class='ayah-box ayah-text'>{v['text']}</div>", f"<div class='surah-badge'>سورة {v['surah_name']} - آية {v['ayah']}</div>", ni
|
| 206 |
|
| 207 |
+
rnd_btn.click(get_rnd, outputs=[text_html, info_html, idx_state])
|
| 208 |
+
go_btn.click(select_specific, inputs=[s_dropdown, a_input], outputs=[text_html, info_html, idx_state])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
# Update Stats Update
|
| 211 |
+
def update_ui_stats(msg, txt, info, idx, sess, total):
|
| 212 |
+
new_stats_html = f"""
|
| 213 |
+
<div class='stats-container'>
|
| 214 |
+
<div class='stat-card'>
|
| 215 |
+
<div class='stat-label'>إجمالي التسجيلات</div>
|
| 216 |
+
<div class='stat-value'>{total}</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div class='stat-card' style='animation: pulse 0.5s'>
|
| 219 |
+
<div class='stat-label'>مساهماتك الآن</div>
|
| 220 |
+
<div class='stat-value'>{sess}</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
"""
|
| 224 |
+
return msg, txt, f"<div class='surah-badge'>{info}</div>", idx, sess, new_stats_html
|
| 225 |
+
|
| 226 |
+
send_btn.click(
|
| 227 |
+
on_submit,
|
| 228 |
+
inputs=[audio_ui, idx_state, sess_state],
|
| 229 |
+
outputs=[gr.Markdown(), text_html, info_html, idx_state, sess_state, gr.State()] # placeholders
|
| 230 |
+
).then(
|
| 231 |
+
lambda sess, tot: f"<div class='stats-container'><div class='stat-card'><div class='stat-label'>إجمالي التسجيلات</div><div class='stat-value'>{tot}</div></div><div class='stat-card'><div class='stat-label'>مساهماتك الآن</div><div class='stat-value'>{sess}</div></div></div>",
|
| 232 |
+
inputs=[sess_state, gr.State(get_total_count())],
|
| 233 |
+
outputs=gr.HTML() # This part needs a specific target, simplified below
|
| 234 |
)
|
| 235 |
|
| 236 |
+
# JavaScript لمنع الخروج
|
| 237 |
+
demo.load(None, None, None, js="""() => {
|
| 238 |
+
document.body.dir = 'rtl';
|
| 239 |
+
window.onbeforeunload = () => 'تنبيه: هل تريد الخروج قبل حفظ تسجيلك؟';
|
| 240 |
+
}""")
|
| 241 |
+
|
| 242 |
+
demo.launch()
|