Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
-
# app.py —
|
| 3 |
|
| 4 |
import os, json, uuid, random, unicodedata
|
| 5 |
from dataclasses import dataclass
|
|
@@ -91,7 +91,7 @@ def norm_ar(t:str)->str:
|
|
| 91 |
t = re2.sub(AR_DIAC, "", t)
|
| 92 |
t = re2.sub(r"[إأآا]", "ا", t)
|
| 93 |
t = re2.sub(r"[يى]", "ي", t)
|
| 94 |
-
t = re2.sub(r"\s+", " ", t)
|
| 95 |
t = re2.sub(r'(\p{L})\1{2,}', r'\1', t)
|
| 96 |
t = re2.sub(r'(\p{L})\1', r'\1', t)
|
| 97 |
return t.strip()
|
|
@@ -198,7 +198,6 @@ def init_state(records):
|
|
| 198 |
|
| 199 |
# ---------- HTML للواجهة/الاختبار ----------
|
| 200 |
def render_quiz_html(records: List[dict]) -> str:
|
| 201 |
-
# يبني HTML فيه كل الأسئلة دفعة واحدة (راديو لكل سؤال)
|
| 202 |
parts = []
|
| 203 |
for i, rec in enumerate(records, start=1):
|
| 204 |
qid = rec["id"]
|
|
@@ -206,9 +205,7 @@ def render_quiz_html(records: List[dict]) -> str:
|
|
| 206 |
opts = rec["options"]
|
| 207 |
opts_html = []
|
| 208 |
for o in opts:
|
| 209 |
-
lid = o["id"]
|
| 210 |
-
txt = o["text"]
|
| 211 |
-
# name = q_<id>, value = letter
|
| 212 |
opts_html.append(f"""
|
| 213 |
<label class="opt">
|
| 214 |
<input type="radio" name="q_{qid}" value="{lid}" />
|
|
@@ -216,23 +213,16 @@ def render_quiz_html(records: List[dict]) -> str:
|
|
| 216 |
<span class="opt-text">{txt}</span>
|
| 217 |
</label>
|
| 218 |
""")
|
| 219 |
-
|
| 220 |
<div class="q-card" data-qid="{qid}">
|
| 221 |
<div class="q-title">السؤال {i}:</div>
|
| 222 |
<div class="q-text">{qtxt}</div>
|
| 223 |
<div class="opts">{''.join(opts_html)}</div>
|
| 224 |
</div>
|
| 225 |
-
"""
|
| 226 |
-
|
| 227 |
|
| 228 |
-
|
| 229 |
-
<div class="quiz-wrap">
|
| 230 |
-
{''.join(parts)}
|
| 231 |
-
</div>
|
| 232 |
-
"""
|
| 233 |
-
return html
|
| 234 |
-
|
| 235 |
-
# ---------- معالجة الإدخال (نص أو ملف) ----------
|
| 236 |
def build_quiz(text_area, file_path, n, model_id, zoom):
|
| 237 |
text_area = (text_area or "").strip()
|
| 238 |
if not text_area and not file_path:
|
|
@@ -289,27 +279,26 @@ def grade(state, answers_json):
|
|
| 289 |
|
| 290 |
return score_md, mistakes_md
|
| 291 |
|
| 292 |
-
# ---------- الثيم (CSS داكن
|
| 293 |
CSS = """
|
| 294 |
:root{
|
| 295 |
--bg:#0f0f0f; --panel:#1a1a1a; --card:#1b1b1b; --muted:#9aa0a6;
|
| 296 |
--text:#f5efe6; --accent:#ff7d2d; --accent2:#ff9a55; --border:#2a2a2a;
|
| 297 |
}
|
| 298 |
body{direction:rtl; font-family:system-ui,'Cairo','IBM Plex Arabic',sans-serif; background:var(--bg);}
|
| 299 |
-
.gradio-container{max-width:
|
| 300 |
.top-title{color:#e9ded6;margin:8px 0 16px 0}
|
| 301 |
-
|
| 302 |
.panel{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px;box-shadow:0 20px 45px rgba(0,0,0,.4)}
|
| 303 |
.small{opacity:.85;color:#ddd}
|
| 304 |
|
| 305 |
-
.button-primary > button{
|
| 306 |
-
background:linear-gradient(180deg,var(--accent2),var(--accent));
|
| 307 |
-
border:none;color:#161616;font-weight:700;
|
| 308 |
-
}
|
| 309 |
.button-primary > button:hover{filter:brightness(.95)}
|
| 310 |
.upload-like{border:2px dashed #ff9a5555;background:#141414;border-radius:12px;padding:10px;color:#ddd}
|
| 311 |
|
| 312 |
-
/*
|
|
|
|
|
|
|
|
|
|
| 313 |
textarea{min-height:140px}
|
| 314 |
|
| 315 |
/* بطاقة السؤال */
|
|
@@ -321,8 +310,6 @@ textarea{min-height:140px}
|
|
| 321 |
.opt input{accent-color:var(--accent)}
|
| 322 |
.opt-letter{display:inline-flex;width:28px;height:28px;border-radius:8px;background:#222;align-items:center;justify-content:center;font-weight:800;color:#f1f1f1}
|
| 323 |
.opt-text{color:#eaeaea}
|
| 324 |
-
|
| 325 |
-
/* قسم النتيجة */
|
| 326 |
.result-card{background:#121212;border:1px solid #2a2a2a;border-radius:16px;padding:16px;margin-top:18px}
|
| 327 |
"""
|
| 328 |
|
|
@@ -330,35 +317,30 @@ textarea{min-height:140px}
|
|
| 330 |
with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
| 331 |
gr.Markdown("<h2 class='top-title'>Question Generator</h2>")
|
| 332 |
|
| 333 |
-
# لوحة
|
| 334 |
-
with gr.Group(elem_classes=["panel"]):
|
| 335 |
-
gr.Markdown("
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
with gr.Column(scale=1):
|
| 352 |
-
text_area = gr.Textbox(lines=6, placeholder="ألصق هنا مقطع نصي...", label="أدخل نصًا أو ارفع ملفًا")
|
| 353 |
-
num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
|
| 354 |
btn_build = gr.Button("توليد الأسئلة", elem_classes=["button-primary"])
|
| 355 |
toast = gr.Markdown("", elem_classes=["small"])
|
| 356 |
|
| 357 |
-
# حالة عامة + مكان عرض الاختبار
|
| 358 |
state = gr.State(None)
|
| 359 |
quiz_html = gr.HTML("") # كل الأسئلة ستُعرض هنا دفعة واحدة
|
| 360 |
-
|
| 361 |
-
# زر الإرسال + حقل سري لتجميع الإجابات + مكان النتيجة
|
| 362 |
btn_submit = gr.Button("إنهاء وإرسال الإجابات", elem_classes=["button-primary"])
|
| 363 |
answers_box = gr.Textbox(visible=False)
|
| 364 |
score_md = gr.Markdown("")
|
|
@@ -371,9 +353,11 @@ with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
|
| 371 |
outputs=[state, quiz_html, toast]
|
| 372 |
)
|
| 373 |
|
| 374 |
-
# JS لالتقاط الإجابات
|
| 375 |
js_collect = """
|
| 376 |
function () {
|
|
|
|
|
|
|
| 377 |
const data = {};
|
| 378 |
document.querySelectorAll('.q-card').forEach(card => {
|
| 379 |
const qid = card.getAttribute('data-qid');
|
|
@@ -384,7 +368,7 @@ with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
|
| 384 |
}
|
| 385 |
"""
|
| 386 |
|
| 387 |
-
#
|
| 388 |
btn_submit.click(
|
| 389 |
None, inputs=None, outputs=[answers_box], js=js_collect
|
| 390 |
).then(
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
+
# app.py — ثيم داكن ثابت + كل الأسئلة دفعة واحدة + Submit لعرض النتيجة والأخطاء
|
| 3 |
|
| 4 |
import os, json, uuid, random, unicodedata
|
| 5 |
from dataclasses import dataclass
|
|
|
|
| 91 |
t = re2.sub(AR_DIAC, "", t)
|
| 92 |
t = re2.sub(r"[إأآا]", "ا", t)
|
| 93 |
t = re2.sub(r"[يى]", "ي", t)
|
| 94 |
+
t = re2.sub(r"\س+", " ", t) if False else re2.sub(r"\s+", " ", t)
|
| 95 |
t = re2.sub(r'(\p{L})\1{2,}', r'\1', t)
|
| 96 |
t = re2.sub(r'(\p{L})\1', r'\1', t)
|
| 97 |
return t.strip()
|
|
|
|
| 198 |
|
| 199 |
# ---------- HTML للواجهة/الاختبار ----------
|
| 200 |
def render_quiz_html(records: List[dict]) -> str:
|
|
|
|
| 201 |
parts = []
|
| 202 |
for i, rec in enumerate(records, start=1):
|
| 203 |
qid = rec["id"]
|
|
|
|
| 205 |
opts = rec["options"]
|
| 206 |
opts_html = []
|
| 207 |
for o in opts:
|
| 208 |
+
lid = o["id"]; txt = o["text"]
|
|
|
|
|
|
|
| 209 |
opts_html.append(f"""
|
| 210 |
<label class="opt">
|
| 211 |
<input type="radio" name="q_{qid}" value="{lid}" />
|
|
|
|
| 213 |
<span class="opt-text">{txt}</span>
|
| 214 |
</label>
|
| 215 |
""")
|
| 216 |
+
parts.append(f"""
|
| 217 |
<div class="q-card" data-qid="{qid}">
|
| 218 |
<div class="q-title">السؤال {i}:</div>
|
| 219 |
<div class="q-text">{qtxt}</div>
|
| 220 |
<div class="opts">{''.join(opts_html)}</div>
|
| 221 |
</div>
|
| 222 |
+
""")
|
| 223 |
+
return f"""<div id="quiz" class="quiz-wrap">{''.join(parts)}</div>"""
|
| 224 |
|
| 225 |
+
# ---------- معالجة الإدخال ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
def build_quiz(text_area, file_path, n, model_id, zoom):
|
| 227 |
text_area = (text_area or "").strip()
|
| 228 |
if not text_area and not file_path:
|
|
|
|
| 279 |
|
| 280 |
return score_md, mistakes_md
|
| 281 |
|
| 282 |
+
# ---------- الثيم (CSS داكن ثابت) ----------
|
| 283 |
CSS = """
|
| 284 |
:root{
|
| 285 |
--bg:#0f0f0f; --panel:#1a1a1a; --card:#1b1b1b; --muted:#9aa0a6;
|
| 286 |
--text:#f5efe6; --accent:#ff7d2d; --accent2:#ff9a55; --border:#2a2a2a;
|
| 287 |
}
|
| 288 |
body{direction:rtl; font-family:system-ui,'Cairo','IBM Plex Arabic',sans-serif; background:var(--bg);}
|
| 289 |
+
.gradio-container{max-width:980px;margin:0 auto;padding:8px 8px 40px;}
|
| 290 |
.top-title{color:#e9ded6;margin:8px 0 16px 0}
|
|
|
|
| 291 |
.panel{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px;box-shadow:0 20px 45px rgba(0,0,0,.4)}
|
| 292 |
.small{opacity:.85;color:#ddd}
|
| 293 |
|
| 294 |
+
.button-primary > button{background:linear-gradient(180deg,var(--accent2),var(--accent));border:none;color:#161616;font-weight:700;}
|
|
|
|
|
|
|
|
|
|
| 295 |
.button-primary > button:hover{filter:brightness(.95)}
|
| 296 |
.upload-like{border:2px dashed #ff9a5555;background:#141414;border-radius:12px;padding:10px;color:#ddd}
|
| 297 |
|
| 298 |
+
/* ترتيب عمودي ثابت */
|
| 299 |
+
.input-stack > div {margin-bottom:12px}
|
| 300 |
+
|
| 301 |
+
/* حقل النص أصغر وثابت */
|
| 302 |
textarea{min-height:140px}
|
| 303 |
|
| 304 |
/* بطاقة السؤال */
|
|
|
|
| 310 |
.opt input{accent-color:var(--accent)}
|
| 311 |
.opt-letter{display:inline-flex;width:28px;height:28px;border-radius:8px;background:#222;align-items:center;justify-content:center;font-weight:800;color:#f1f1f1}
|
| 312 |
.opt-text{color:#eaeaea}
|
|
|
|
|
|
|
| 313 |
.result-card{background:#121212;border:1px solid #2a2a2a;border-radius:16px;padding:16px;margin-top:18px}
|
| 314 |
"""
|
| 315 |
|
|
|
|
| 317 |
with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
| 318 |
gr.Markdown("<h2 class='top-title'>Question Generator</h2>")
|
| 319 |
|
| 320 |
+
# لوحة إدخال عمودية ثابتة (لا تتقلب)
|
| 321 |
+
with gr.Group(elem_classes=["panel","input-stack"]):
|
| 322 |
+
gr.Markdown("**أدخل نصًا أو ارفع ملفًا، حدّد عدد الأسئلة ثم اضغط توليد.**", elem_classes=["small"])
|
| 323 |
+
text_area = gr.Textbox(lines=6, placeholder="ألصق هنا مقطع نصي...", label="أدخل نصًا أو ارفع ملفًا")
|
| 324 |
+
num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
|
| 325 |
+
file_comp = gr.File(label="اختر ملف PDF أو TXT", file_count="single",
|
| 326 |
+
file_types=[".pdf",".txt"], type="filepath", elem_classes=["upload-like"])
|
| 327 |
+
with gr.Accordion("خيارات متقدمة (لـ PDF المصوّر)", open=False):
|
| 328 |
+
trocr_model = gr.Dropdown(
|
| 329 |
+
choices=[
|
| 330 |
+
"microsoft/trocr-base-printed",
|
| 331 |
+
"microsoft/trocr-large-printed",
|
| 332 |
+
"microsoft/trocr-base-handwritten",
|
| 333 |
+
"microsoft/trocr-large-handwritten",
|
| 334 |
+
],
|
| 335 |
+
value=DEFAULT_TROCR_MODEL, label="نموذج TrOCR"
|
| 336 |
+
)
|
| 337 |
+
trocr_zoom = gr.Slider(2.0, 3.5, value=DEFAULT_TROCR_ZOOM, step=0.1, label="Zoom OCR")
|
|
|
|
|
|
|
|
|
|
| 338 |
btn_build = gr.Button("توليد الأسئلة", elem_classes=["button-primary"])
|
| 339 |
toast = gr.Markdown("", elem_classes=["small"])
|
| 340 |
|
| 341 |
+
# حالة عامة + مكان عرض الاختبار + إرساله
|
| 342 |
state = gr.State(None)
|
| 343 |
quiz_html = gr.HTML("") # كل الأسئلة ستُعرض هنا دفعة واحدة
|
|
|
|
|
|
|
| 344 |
btn_submit = gr.Button("إنهاء وإرسال الإجابات", elem_classes=["button-primary"])
|
| 345 |
answers_box = gr.Textbox(visible=False)
|
| 346 |
score_md = gr.Markdown("")
|
|
|
|
| 353 |
outputs=[state, quiz_html, toast]
|
| 354 |
)
|
| 355 |
|
| 356 |
+
# JS لالتقاط الإجابات + سكرول للأسئلة بعد التوليد
|
| 357 |
js_collect = """
|
| 358 |
function () {
|
| 359 |
+
const quiz = document.getElementById('quiz');
|
| 360 |
+
if (quiz) { quiz.scrollIntoView({behavior:'smooth', block:'start'}); }
|
| 361 |
const data = {};
|
| 362 |
document.querySelectorAll('.q-card').forEach(card => {
|
| 363 |
const qid = card.getAttribute('data-qid');
|
|
|
|
| 368 |
}
|
| 369 |
"""
|
| 370 |
|
| 371 |
+
# Submit: نجمع الإجابات بالـJS ثم نقيّمها
|
| 372 |
btn_submit.click(
|
| 373 |
None, inputs=None, outputs=[answers_box], js=js_collect
|
| 374 |
).then(
|