Leen172 commited on
Commit
4db5a7f
·
verified ·
1 Parent(s): 085322d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -75
app.py CHANGED
@@ -1,10 +1,10 @@
1
  # -*- coding: utf-8 -*-
2
- # صفحتان ثابتتان + Submit لكل سؤال (محاولات متعددة) + واجهة إدخال لا تتغير أبعادها بعد رفع الملف
3
 
4
- import os, json, uuid, random, unicodedata
5
  from dataclasses import dataclass
6
  from pathlib import Path
7
- from typing import List, Tuple
8
 
9
  from PIL import Image
10
  from pypdf import PdfReader
@@ -81,7 +81,7 @@ def strip_headers(t:str)->str:
81
  out=[]
82
  for ln in t.splitlines():
83
  if re2.match(r"^\s*--- \[Page \d+\] ---\s*$", ln): continue
84
- if re2.match(r"^\s*(Page\s*\d+|صفحة\s*\د+)\s*$", ln): continue
85
  if re2.match(r"^\s*[-–—_*]{3,}\s*$", ln): continue
86
  out.append(ln)
87
  return "\n".join(out)
@@ -92,7 +92,7 @@ def norm_ar(t:str)->str:
92
  t = re2.sub(AR_DIAC, "", t)
93
  t = re2.sub(r"[إأآا]", "ا", t)
94
  t = re2.sub(r"[يى]", "ي", t)
95
- t = re2.sub(r"\s+", " ", t)
96
  t = re2.sub(r'(\p{L})\1{2,}', r'\1', t)
97
  t = re2.sub(r'(\p{L})\1', r'\1', t)
98
  return t.strip()
@@ -178,7 +178,7 @@ def to_records(items:List[MCQ])->List[dict]:
178
  recs.append({"id":it.id,"question":it.question.strip(),"options":opts})
179
  return recs
180
 
181
- # ------------------ صفحة الأسئلة (HTML فقط) ------------------
182
  def render_quiz_html(records: List[dict]) -> str:
183
  parts=[]
184
  for i, rec in enumerate(records, start=1):
@@ -211,21 +211,58 @@ def render_quiz_html(records: List[dict]) -> str:
211
  """)
212
  return f"""<div id="quiz" class="quiz-wrap">{''.join(parts)}</div>"""
213
 
214
- # ------------------ توليد الامتحان وتبديل الصفحات ------------------
215
- def build_quiz(text_area, file_path, n, model_id, zoom):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  text_area = (text_area or "").strip()
217
- if not text_area and not file_path:
218
- return "", gr.update(visible=True), gr.update(visible=False), "🛈 الصق نصًا أو ارفع ملفًا أولًا."
219
  if text_area:
220
  raw = text_area
 
 
 
 
 
 
 
 
 
221
  else:
222
- raw, _ = file_to_text(file_path, model_id=model_id, zoom=float(zoom))
 
223
  cleaned = postprocess(raw)
224
  items = make_mcqs(cleaned, n=int(n))
225
  recs = to_records(items)
226
  return render_quiz_html(recs), gr.update(visible=False), gr.update(visible=True), ""
227
 
228
- # ------------------ CSS (ثبات واجهة الإدخال + منع تمدد مكوّن الملف) ------------------
229
  CSS = """
230
  :root{
231
  --bg:#0e0e11; --panel:#15161a; --card:#1a1b20; --muted:#a7b0be;
@@ -235,67 +272,30 @@ body{direction:rtl; font-family:system-ui,'Cairo','IBM Plex Arabic',sans-serif;
235
  .gradio-container{max-width:980px;margin:0 auto;padding:12px 12px 40px;}
236
  h2.top{color:#eaeaf2;margin:6px 0 16px}
237
 
238
- /* 1) لوحة الإدخال ثابتة الارتفاع */
239
  .input-panel{
240
  background:var(--panel);
241
  border:1px solid var(--border);
242
  border-radius:14px;
243
  padding:16px;
244
  box-shadow:0 16px 38px rgba(0,0,0,.35);
245
- min-height:420px; /* ارتفاع ثابت مريح */
246
  display:flex; flex-direction:column; gap:12px;
247
  }
248
 
249
- /* 2) حاوية إسقاط ثابتة الارتفاع تمنع أي تمدد */
250
- .drop-wrap{
251
- position:relative;
252
- height:120px; /* ثبّت ارتفاع منطقة الرفع */
253
- min-height:120px; max-height:120px;
254
- overflow:hidden; /* قصّ أي معاينات */
255
- border:2px dashed #3b3f52;
256
- background:#121318;
257
- border-radius:12px;
258
- padding:10px;
259
- color:#cfd5e3;
260
  }
261
-
262
- /* 3) امنع مكوّن الملف من تغيير الحجم داخليًا مهما أضاف معاينات */
263
- .drop-wrap [data-testid="file"]{
264
- position:absolute; inset:0;
265
- height:100%; width:100%;
266
- overflow:hidden !important;
267
- }
268
- .drop-wrap [data-testid="file"] *{ max-height:100% !important; }
269
-
270
- /* 4) إخفاء أي معاينة/شبكة/اسم ملف طويلة قد تضيفها إصدارات مختلفة من Gradio */
271
- .drop-wrap [class*="preview"],
272
- .drop-wrap [class*="file-preview"],
273
- .drop-wrap .file-preview,
274
- .drop-wrap .file-preview * ,
275
- .drop-wrap .upload-preview,
276
- .drop-wrap .grid,
277
- .drop-wrap .grid-wrap,
278
- .drop-wrap .thumbnail,
279
- .drop-wrap .thumbnails,
280
- .drop-wrap .label,
281
- .drop-wrap .hidden,
282
- .drop-wrap .file-name,
283
- .drop-wrap .file-info,
284
- .drop-wrap .file-size {
285
- display:none !important;
286
- }
287
-
288
- /* 5) اترك منطقة النقر على الرفع فعّالة على المساحة كلها */
289
- .drop-wrap input[type="file"]{
290
- position:absolute; inset:0;
291
- height:100%; width:100%;
292
- opacity:0; cursor:pointer;
293
  }
294
-
295
- /* عناصر أخرى */
296
- .small{opacity:.9;color:#d9dee8}
297
  .button-primary>button{background:linear-gradient(180deg,var(--accent),var(--accent2));border:none;color:#0b0d10;font-weight:800}
298
  .button-primary>button:hover{filter:brightness(.95)}
 
299
  textarea{min-height:120px}
300
 
301
  /* صفحة الأسئلة */
@@ -312,7 +312,7 @@ textarea{min-height:120px}
312
  .opt input{accent-color:var(--accent2)}
313
  .opt-letter{display:inline-flex;width:28px;height:28px;border-radius:8px;background:#0f1116;border:1px solid #2a2d3a;align-items:center;justify-content:center;font-weight:800;color:#dfe6f7}
314
  .opt-text{color:#eaeaf2}
315
- .opt.ok{background:#0f2f22;border-color:#1b6a52}
316
  .opt.err{background:#3a0d14;border-color:#6a1e2b}
317
 
318
  .q-actions{display:flex;gap:10px;align-items:center;margin-top:10px}
@@ -321,7 +321,7 @@ textarea{min-height:120px}
321
  }
322
  .q-actions .q-submit:disabled{opacity:.5;cursor:not-allowed}
323
  .q-note{color:#ffd1d6}
324
- .q-note.warn{color:#ffd1د6}
325
  """
326
 
327
  # ------------------ JS: Submit (محاولات متعددة) ------------------
@@ -359,7 +359,7 @@ ATTACH_LISTENERS_JS = """
359
  return;
360
  }
361
 
362
- // خطأ: أحمر فقط، ولا تعطيل لأي عنصر تسمح بمحاولات غير محدودة
363
  chosenLabel.classList.add('err');
364
  if (badge){ badge.hidden=false; badge.className='q-badge err'; badge.textContent='Incorrect.'; }
365
  if (note) note.textContent = '';
@@ -373,28 +373,26 @@ ATTACH_LISTENERS_JS = """
373
  with gr.Blocks(title="Question Generator", css=CSS) as demo:
374
  gr.Markdown("<h2 class='top'>Question Generator</h2>")
375
 
 
 
 
 
376
  # الصفحة 1: إدخال ثابت لا تتغير أبعاده
377
  page1 = gr.Group(visible=True, elem_classes=["input-panel"])
378
  with page1:
379
  gr.Markdown("اختر **أحد** الخيارين ثم اضغط الزر.", elem_classes=["small"])
380
  text_area = gr.Textbox(lines=6, placeholder="ألصق نصك هنا...", label="لصق نص")
381
 
382
- # مكوّن الملف داخل حاوية drop-wrap لتثبيت الارتفاع
383
- with gr.Box(elem_classes=["drop-wrap"]):
384
- file_comp = gr.File(
385
- label="أو ارفع ملف (PDF / TXT)",
386
- file_count="single",
387
- file_types=[".pdf", ".txt"],
388
- type="filepath",
389
- elem_classes=[],
390
- show_label=False
391
- )
392
 
393
  num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
394
  with gr.Accordion("خيارات PDF المصوّر (اختياري)", open=False):
395
  trocr_model = gr.Dropdown(
396
  choices=[
397
- "مicrosoft/trocr-base-printed",
398
  "microsoft/trocr-large-printed",
399
  "microsoft/trocr-base-handwritten",
400
  "microsoft/trocr-large-handwritten",
@@ -411,10 +409,17 @@ with gr.Blocks(title="Question Generator", css=CSS) as demo:
411
  quiz_html = gr.HTML("")
412
  js_wired = gr.Textbox(visible=False) # Output مخفي لضمان تنفيذ JS
413
 
 
 
 
 
 
 
 
414
  # بناء الامتحان + تبديل الصفحات + ربط الـJS
415
  btn_build.click(
416
  build_quiz,
417
- inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
418
  outputs=[quiz_html, page1, page2, warn]
419
  ).then(
420
  None, inputs=None, outputs=[js_wired], js=ATTACH_LISTENERS_JS
 
1
  # -*- coding: utf-8 -*-
2
+ # صفحتان ثابتتان + Submit لكل سؤال (محاولات متعددة) + واجهة إدخال ثابتة الأبعاد باستخدام UploadButton
3
 
4
+ import os, json, uuid, random, unicodedata, tempfile
5
  from dataclasses import dataclass
6
  from pathlib import Path
7
+ from typing import List, Tuple, Optional
8
 
9
  from PIL import Image
10
  from pypdf import PdfReader
 
81
  out=[]
82
  for ln in t.splitlines():
83
  if re2.match(r"^\s*--- \[Page \d+\] ---\s*$", ln): continue
84
+ if re2.match(r"^\s*(Page\s*\d+|صفحة\s*\d+)\s*$", ln): continue
85
  if re2.match(r"^\s*[-–—_*]{3,}\s*$", ln): continue
86
  out.append(ln)
87
  return "\n".join(out)
 
92
  t = re2.sub(AR_DIAC, "", t)
93
  t = re2.sub(r"[إأآا]", "ا", t)
94
  t = re2.sub(r"[يى]", "ي", t)
95
+ t = re2.sub(r"\س+", " ", t)
96
  t = re2.sub(r'(\p{L})\1{2,}', r'\1', t)
97
  t = re2.sub(r'(\p{L})\1', r'\1', t)
98
  return t.strip()
 
178
  recs.append({"id":it.id,"question":it.question.strip(),"options":opts})
179
  return recs
180
 
181
+ # ------------------ HTML صفحة الأسئلة ------------------
182
  def render_quiz_html(records: List[dict]) -> str:
183
  parts=[]
184
  for i, rec in enumerate(records, start=1):
 
211
  """)
212
  return f"""<div id="quiz" class="quiz-wrap">{''.join(parts)}</div>"""
213
 
214
+ # ------------------ دعم UploadButton بدلاً من File ------------------
215
+ # نخزن الملف في State كـ (bytes, filename)
216
+ def on_upload(files) -> Tuple[Optional[bytes], str, str]:
217
+ """
218
+ files: يمكن أن يكون ملفًا واحدًا أو قائمة.
219
+ نعيد: (file_bytes, file_name, رسالة حالة قصيرة)
220
+ """
221
+ if not files:
222
+ return None, "", "لم يتم اختيار ملف."
223
+ f = files[0] if isinstance(files, list) else files
224
+ name = getattr(f, "name", "uploaded_file")
225
+ # Gradio قد يعطي مسارًا مؤقتًا أو BytesIO. نجرب القراءة:
226
+ data = None
227
+ try:
228
+ if hasattr(f, "read"):
229
+ data = f.read()
230
+ elif hasattr(f, "bytes"):
231
+ data = f.bytes
232
+ elif isinstance(f, (bytes, bytearray)):
233
+ data = bytes(f)
234
+ elif isinstance(f, str) and os.path.isfile(f):
235
+ with open(f, "rb") as fp:
236
+ data = fp.read()
237
+ except Exception:
238
+ data = None
239
+ if not data:
240
+ return None, "", "تعذّر قراءة الملف. جرّب مجددًا."
241
+ return data, name, f"تم اختيار: {Path(name).name}"
242
+
243
+ def build_quiz(text_area, file_bytes, file_name, n, model_id, zoom):
244
  text_area = (text_area or "").strip()
245
+ raw = ""
 
246
  if text_area:
247
  raw = text_area
248
+ elif file_bytes:
249
+ # نكتب bytes إلى ملف مؤقت بالامتداد الصحيح
250
+ suffix = Path(file_name).suffix or ".pdf"
251
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
252
+ tf.write(file_bytes)
253
+ tmp_path = tf.name
254
+ raw, _ = file_to_text(tmp_path, model_id=model_id, zoom=float(zoom))
255
+ try: os.remove(tmp_path)
256
+ except Exception: pass
257
  else:
258
+ return "", gr.update(visible=True), gr.update(visible=False), "🛈 الصق نصًا أو ارفع ملفًا أولًا."
259
+
260
  cleaned = postprocess(raw)
261
  items = make_mcqs(cleaned, n=int(n))
262
  recs = to_records(items)
263
  return render_quiz_html(recs), gr.update(visible=False), gr.update(visible=True), ""
264
 
265
+ # ------------------ CSS (ثبات واجهة الإدخال تمامًا) ------------------
266
  CSS = """
267
  :root{
268
  --bg:#0e0e11; --panel:#15161a; --card:#1a1b20; --muted:#a7b0be;
 
272
  .gradio-container{max-width:980px;margin:0 auto;padding:12px 12px 40px;}
273
  h2.top{color:#eaeaf2;margin:6px 0 16px}
274
 
275
+ /* لوحة الإدخال ثابتة تمامًا */
276
  .input-panel{
277
  background:var(--panel);
278
  border:1px solid var(--border);
279
  border-radius:14px;
280
  padding:16px;
281
  box-shadow:0 16px 38px rgba(0,0,0,.35);
282
+ min-height:420px; /* ثبات */
283
  display:flex; flex-direction:column; gap:12px;
284
  }
285
 
286
+ /* صف رفع بسيط لا يتغيّر حجمه */
287
+ .uprow{
288
+ display:flex; gap:10px; align-items:center;
 
 
 
 
 
 
 
 
289
  }
290
+ .file-pill{
291
+ background:#121318; border:1px dashed #3b3f52; color:#cfd5e3;
292
+ border-radius:10px; padding:10px 12px; flex:1; min-height:48px;
293
+ display:flex; align-items:center;
294
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
 
 
 
296
  .button-primary>button{background:linear-gradient(180deg,var(--accent),var(--accent2));border:none;color:#0b0d10;font-weight:800}
297
  .button-primary>button:hover{filter:brightness(.95)}
298
+ .small{opacity:.9;color:#d9dee8}
299
  textarea{min-height:120px}
300
 
301
  /* صفحة الأسئلة */
 
312
  .opt input{accent-color:var(--accent2)}
313
  .opt-letter{display:inline-flex;width:28px;height:28px;border-radius:8px;background:#0f1116;border:1px solid #2a2d3a;align-items:center;justify-content:center;font-weight:800;color:#dfe6f7}
314
  .opt-text{color:#eaeaf2}
315
+ .opt.ok{background:#0ف2f22;border-color:#1b6a52}
316
  .opt.err{background:#3a0d14;border-color:#6a1e2b}
317
 
318
  .q-actions{display:flex;gap:10px;align-items:center;margin-top:10px}
 
321
  }
322
  .q-actions .q-submit:disabled{opacity:.5;cursor:not-allowed}
323
  .q-note{color:#ffd1d6}
324
+ .q-note.warn{color:#ffd1d6}
325
  """
326
 
327
  # ------------------ JS: Submit (محاولات متعددة) ------------------
 
359
  return;
360
  }
361
 
362
+ // خطأ: أحمر فقط، ولا تعطيل — يسمح بمحاولات متتالية
363
  chosenLabel.classList.add('err');
364
  if (badge){ badge.hidden=false; badge.className='q-badge err'; badge.textContent='Incorrect.'; }
365
  if (note) note.textContent = '';
 
373
  with gr.Blocks(title="Question Generator", css=CSS) as demo:
374
  gr.Markdown("<h2 class='top'>Question Generator</h2>")
375
 
376
+ # States لحفظ الملف المرفوع عبر UploadButton
377
+ file_bytes_state = gr.State(None)
378
+ file_name_state = gr.State("")
379
+
380
  # الصفحة 1: إدخال ثابت لا تتغير أبعاده
381
  page1 = gr.Group(visible=True, elem_classes=["input-panel"])
382
  with page1:
383
  gr.Markdown("اختر **أحد** الخيارين ثم اضغط الزر.", elem_classes=["small"])
384
  text_area = gr.Textbox(lines=6, placeholder="ألصق نصك هنا...", label="لصق نص")
385
 
386
+ # صف رفع بسيط: زر + شارة اسم ملف ثابتة العرض
387
+ with gr.Row(elem_classes=["uprow"]):
388
+ upload_btn = gr.UploadButton("رفع ملف (PDF / TXT)", file_types=[".pdf", ".txt"], file_count="single")
389
+ file_badge = gr.Markdown("لم يتم اختيار ملف", elem_classes=["file-pill"])
 
 
 
 
 
 
390
 
391
  num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
392
  with gr.Accordion("خيارات PDF المصوّر (اختياري)", open=False):
393
  trocr_model = gr.Dropdown(
394
  choices=[
395
+ "microsoft/trocr-base-printed",
396
  "microsoft/trocr-large-printed",
397
  "microsoft/trocr-base-handwritten",
398
  "microsoft/trocr-large-handwritten",
 
409
  quiz_html = gr.HTML("")
410
  js_wired = gr.Textbox(visible=False) # Output مخفي لضمان تنفيذ JS
411
 
412
+ # حدث رفع الملف → خزِّن بالـState + عرض الاسم في الشارة
413
+ upload_btn.upload(
414
+ fn=on_upload,
415
+ inputs=[upload_btn],
416
+ outputs=[file_bytes_state, file_name_state, file_badge]
417
+ )
418
+
419
  # بناء الامتحان + تبديل الصفحات + ربط الـJS
420
  btn_build.click(
421
  build_quiz,
422
+ inputs=[text_area, file_bytes_state, file_name_state, num_q, trocr_model, trocr_zoom],
423
  outputs=[quiz_html, page1, page2, warn]
424
  ).then(
425
  None, inputs=None, outputs=[js_wired], js=ATTACH_LISTENERS_JS