Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
-
#
|
| 3 |
-
|
| 4 |
import os, json, uuid, random, unicodedata
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from pathlib import Path
|
|
@@ -17,7 +16,7 @@ import gradio as gr
|
|
| 17 |
random.seed(42)
|
| 18 |
DEFAULT_NUM_QUESTIONS = 6
|
| 19 |
DEFAULT_TROCR_MODEL = "microsoft/trocr-base-printed"
|
| 20 |
-
DEFAULT_TROCR_ZOOM
|
| 21 |
|
| 22 |
# ------------------ OCR (تحميل كسول) ------------------
|
| 23 |
_OCR = {}
|
|
@@ -34,8 +33,10 @@ def extract_text_with_pypdf(path: str) -> str:
|
|
| 34 |
reader = PdfReader(path)
|
| 35 |
out = []
|
| 36 |
for p in reader.pages:
|
| 37 |
-
try:
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
out.append(t)
|
| 40 |
return "\n".join(out).strip()
|
| 41 |
|
|
@@ -68,7 +69,8 @@ def is_good(t: str, min_chars=250, min_alpha=0.15) -> bool:
|
|
| 68 |
def file_to_text(path: str, model_id=DEFAULT_TROCR_MODEL, zoom=DEFAULT_TROCR_ZOOM) -> Tuple[str,str]:
|
| 69 |
ext = Path(path).suffix.lower()
|
| 70 |
if ext == ".txt":
|
| 71 |
-
with open(path,"r",encoding="utf-8",errors="ignore") as f:
|
|
|
|
| 72 |
raw = extract_text_with_pypdf(path)
|
| 73 |
if is_good(raw): return raw, "embedded (pypdf)"
|
| 74 |
return extract_text_with_ocr(path, model_id, zoom), "OCR (TrOCR)"
|
|
@@ -176,11 +178,11 @@ def to_records(items:List[MCQ])->List[dict]:
|
|
| 176 |
recs.append({"id":it.id,"question":it.question.strip(),"options":opts})
|
| 177 |
return recs
|
| 178 |
|
| 179 |
-
# ------------------
|
| 180 |
def render_quiz_html(records: List[dict]) -> str:
|
| 181 |
parts=[]
|
| 182 |
for i, rec in enumerate(records, start=1):
|
| 183 |
-
qid
|
| 184 |
qtxt = rec["question"]
|
| 185 |
cor = next((o["id"] for o in rec["options"] if o["is_correct"]), "")
|
| 186 |
opts_html=[]
|
|
@@ -207,50 +209,9 @@ def render_quiz_html(records: List[dict]) -> str:
|
|
| 207 |
</div>
|
| 208 |
</div>
|
| 209 |
""")
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
</div>
|
| 214 |
-
<script>
|
| 215 |
-
document.querySelectorAll('.q-card .q-submit').forEach(btn => {{
|
| 216 |
-
btn.addEventListener('click', (e) => {{
|
| 217 |
-
const card = e.target.closest('.q-card');
|
| 218 |
-
const qid = card.getAttribute('data-qid');
|
| 219 |
-
const correct = card.getAttribute('data-correct');
|
| 220 |
-
const note = document.getElementById('n_'+qid);
|
| 221 |
-
const badge = document.getElementById('b_'+qid);
|
| 222 |
-
const chosenInput = card.querySelector('input[type="radio"]:checked');
|
| 223 |
-
|
| 224 |
-
if (!chosenInput) {{
|
| 225 |
-
if (note) {{ note.textContent = 'اختر إجابة أولاً'; note.className='q-note warn'; }}
|
| 226 |
-
return;
|
| 227 |
-
}}
|
| 228 |
-
|
| 229 |
-
// تنظيف ألوان سابقة
|
| 230 |
-
card.querySelectorAll('.opt').forEach(l => l.classList.remove('ok','err'));
|
| 231 |
-
|
| 232 |
-
const chosen = chosenInput.value;
|
| 233 |
-
const chosenLabel = chosenInput.closest('.opt');
|
| 234 |
-
|
| 235 |
-
if (chosen === correct) {{
|
| 236 |
-
chosenLabel.classList.add('ok');
|
| 237 |
-
if (badge) {{ badge.hidden=false; badge.className='q-badge ok'; badge.textContent='Correct!'; }}
|
| 238 |
-
}} else {{
|
| 239 |
-
chosenLabel.classList.add('err');
|
| 240 |
-
if (badge) {{ badge.hidden=false; badge.className='q-badge err'; badge.textContent='Incorrect.'; }}
|
| 241 |
-
}}
|
| 242 |
-
|
| 243 |
-
// قفل هذا السؤال فقط
|
| 244 |
-
card.querySelectorAll('input[type="radio"]').forEach(i => i.disabled = true);
|
| 245 |
-
e.target.disabled = true;
|
| 246 |
-
if (note) note.textContent = '';
|
| 247 |
-
}});
|
| 248 |
-
}});
|
| 249 |
-
</script>
|
| 250 |
-
"""
|
| 251 |
-
return html
|
| 252 |
-
|
| 253 |
-
# ------------------ بناء الامتحان (من الصفحة الأولى للثانية) ------------------
|
| 254 |
def build_quiz(text_area, file_path, n, model_id, zoom):
|
| 255 |
text_area = (text_area or "").strip()
|
| 256 |
if not text_area and not file_path:
|
|
@@ -260,11 +221,11 @@ def build_quiz(text_area, file_path, n, model_id, zoom):
|
|
| 260 |
else:
|
| 261 |
raw, _ = file_to_text(file_path, model_id=model_id, zoom=float(zoom))
|
| 262 |
cleaned = postprocess(raw)
|
| 263 |
-
items
|
| 264 |
-
recs
|
| 265 |
return render_quiz_html(recs), gr.update(visible=False), gr.update(visible=True), ""
|
| 266 |
|
| 267 |
-
#
|
| 268 |
CSS = """
|
| 269 |
:root{
|
| 270 |
--bg:#0e0e11; --panel:#15161a; --card:#1a1b20; --muted:#a7b0be;
|
|
@@ -277,7 +238,6 @@ h2.top{color:#eaeaf2;margin:6px 0 16px}
|
|
| 277 |
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px;box-shadow:0 16px 38px rgba(0,0,0,.35)}
|
| 278 |
.small{opacity:.9;color:#d9dee8}
|
| 279 |
.upload-like{border:2px dashed #3b3f52;background:#121318;border-radius:12px;padding:10px;color:#cfd5e3}
|
| 280 |
-
|
| 281 |
.button-primary>button{background:linear-gradient(180deg,var(--accent),var(--accent2));border:none;color:#0b0d10;font-weight:800}
|
| 282 |
.button-primary>button:hover{filter:brightness(.95)}
|
| 283 |
textarea{min-height:120px}
|
|
@@ -308,14 +268,55 @@ textarea{min-height:120px}
|
|
| 308 |
.q-note.warn{color:#ffd1d6}
|
| 309 |
"""
|
| 310 |
|
| 311 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
| 313 |
gr.Markdown("<h2 class='top'>Question Generator</h2>")
|
| 314 |
|
| 315 |
-
# الصفحة 1
|
| 316 |
page1 = gr.Group(visible=True, elem_classes=["panel"])
|
| 317 |
with page1:
|
| 318 |
-
gr.Markdown("اختر **أحد** الخيارين
|
| 319 |
text_area = gr.Textbox(lines=6, placeholder="ألصق نصك هنا...", label="لصق نص")
|
| 320 |
file_comp = gr.File(label="أو ارفع ملف (PDF / TXT)", file_count="single",
|
| 321 |
file_types=[".pdf",".txt"], type="filepath", elem_classes=["upload-like"])
|
|
@@ -332,18 +333,21 @@ with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
|
| 332 |
)
|
| 333 |
trocr_zoom = gr.Slider(2.0, 3.5, value=DEFAULT_TROCR_ZOOM, step=0.1, label="Zoom OCR")
|
| 334 |
btn_build = gr.Button("generate quistion", elem_classes=["button-primary"])
|
| 335 |
-
|
| 336 |
|
| 337 |
-
# الصفحة 2
|
| 338 |
page2 = gr.Group(visible=False)
|
| 339 |
with page2:
|
| 340 |
quiz_html = gr.HTML("")
|
| 341 |
|
| 342 |
-
#
|
| 343 |
btn_build.click(
|
| 344 |
build_quiz,
|
| 345 |
inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
|
| 346 |
-
outputs=[quiz_html, page1, page2,
|
|
|
|
|
|
|
|
|
|
| 347 |
)
|
| 348 |
|
| 349 |
if __name__ == "__main__":
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
+
# صفحتان: إدخال بسيط + صفحة أسئلة مع Submit لكل سؤال
|
|
|
|
| 3 |
import os, json, uuid, random, unicodedata
|
| 4 |
from dataclasses import dataclass
|
| 5 |
from pathlib import Path
|
|
|
|
| 16 |
random.seed(42)
|
| 17 |
DEFAULT_NUM_QUESTIONS = 6
|
| 18 |
DEFAULT_TROCR_MODEL = "microsoft/trocr-base-printed"
|
| 19 |
+
DEFAULT_TROCR_ZOOM = 2.6
|
| 20 |
|
| 21 |
# ------------------ OCR (تحميل كسول) ------------------
|
| 22 |
_OCR = {}
|
|
|
|
| 33 |
reader = PdfReader(path)
|
| 34 |
out = []
|
| 35 |
for p in reader.pages:
|
| 36 |
+
try:
|
| 37 |
+
t = p.extract_text() or ""
|
| 38 |
+
except Exception:
|
| 39 |
+
t = ""
|
| 40 |
out.append(t)
|
| 41 |
return "\n".join(out).strip()
|
| 42 |
|
|
|
|
| 69 |
def file_to_text(path: str, model_id=DEFAULT_TROCR_MODEL, zoom=DEFAULT_TROCR_ZOOM) -> Tuple[str,str]:
|
| 70 |
ext = Path(path).suffix.lower()
|
| 71 |
if ext == ".txt":
|
| 72 |
+
with open(path,"r",encoding="utf-8",errors="ignore") as f:
|
| 73 |
+
return f.read(), "plain text"
|
| 74 |
raw = extract_text_with_pypdf(path)
|
| 75 |
if is_good(raw): return raw, "embedded (pypdf)"
|
| 76 |
return extract_text_with_ocr(path, model_id, zoom), "OCR (TrOCR)"
|
|
|
|
| 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):
|
| 185 |
+
qid = rec["id"]
|
| 186 |
qtxt = rec["question"]
|
| 187 |
cor = next((o["id"] for o in rec["options"] if o["is_correct"]), "")
|
| 188 |
opts_html=[]
|
|
|
|
| 209 |
</div>
|
| 210 |
</div>
|
| 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:
|
|
|
|
| 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;
|
|
|
|
| 238 |
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px;box-shadow:0 16px 38px rgba(0,0,0,.35)}
|
| 239 |
.small{opacity:.9;color:#d9dee8}
|
| 240 |
.upload-like{border:2px dashed #3b3f52;background:#121318;border-radius:12px;padding:10px;color:#cfd5e3}
|
|
|
|
| 241 |
.button-primary>button{background:linear-gradient(180deg,var(--accent),var(--accent2));border:none;color:#0b0d10;font-weight:800}
|
| 242 |
.button-primary>button:hover{filter:brightness(.95)}
|
| 243 |
textarea{min-height:120px}
|
|
|
|
| 268 |
.q-note.warn{color:#ffd1d6}
|
| 269 |
"""
|
| 270 |
|
| 271 |
+
# ------------------ JS: ربط أزرار Submit بعد الرندر ------------------
|
| 272 |
+
ATTACH_LISTENERS_JS = """
|
| 273 |
+
() => {
|
| 274 |
+
const bind = (btn) => {
|
| 275 |
+
if (btn.dataset.bound === '1') return;
|
| 276 |
+
btn.dataset.bound = '1';
|
| 277 |
+
btn.addEventListener('click', (e) => {
|
| 278 |
+
const card = e.target.closest('.q-card');
|
| 279 |
+
const qid = card.getAttribute('data-qid');
|
| 280 |
+
const correct= card.getAttribute('data-correct');
|
| 281 |
+
const note = document.getElementById('n_'+qid);
|
| 282 |
+
const badge = document.getElementById('b_'+qid);
|
| 283 |
+
const chosen = card.querySelector('input[type="radio"]:checked');
|
| 284 |
+
|
| 285 |
+
if (!chosen) {
|
| 286 |
+
if (note){ note.textContent = 'اختر إجابة أولاً'; note.className='q-note warn'; }
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
// نظّف ألوان سابقة
|
| 290 |
+
card.querySelectorAll('.opt').forEach(l => l.classList.remove('ok','err'));
|
| 291 |
+
|
| 292 |
+
const chosenLabel = chosen.closest('.opt');
|
| 293 |
+
if (chosen.value === correct) {
|
| 294 |
+
chosenLabel.classList.add('ok');
|
| 295 |
+
if (badge){ badge.hidden=false; badge.className='q-badge ok'; badge.textContent='Correct!'; }
|
| 296 |
+
} else {
|
| 297 |
+
chosenLabel.classList.add('err');
|
| 298 |
+
if (badge){ badge.hidden=false; badge.className='q-badge err'; badge.textContent='Incorrect.'; }
|
| 299 |
+
}
|
| 300 |
+
// أقفل السؤال
|
| 301 |
+
card.querySelectorAll('input[type="radio"]').forEach(i => i.disabled = true);
|
| 302 |
+
e.target.disabled = true;
|
| 303 |
+
if (note) note.textContent = '';
|
| 304 |
+
});
|
| 305 |
+
};
|
| 306 |
+
|
| 307 |
+
document.querySelectorAll('.q-card .q-submit').forEach(bind);
|
| 308 |
+
return [];
|
| 309 |
+
}
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
# ------------------ واجهة Gradio ------------------
|
| 313 |
with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
| 314 |
gr.Markdown("<h2 class='top'>Question Generator</h2>")
|
| 315 |
|
| 316 |
+
# الصفحة 1
|
| 317 |
page1 = gr.Group(visible=True, elem_classes=["panel"])
|
| 318 |
with page1:
|
| 319 |
+
gr.Markdown("اختر **أحد** الخيارين ثم اضغط الزر.", elem_classes=["small"])
|
| 320 |
text_area = gr.Textbox(lines=6, placeholder="ألصق نصك هنا...", label="لصق نص")
|
| 321 |
file_comp = gr.File(label="أو ارفع ملف (PDF / TXT)", file_count="single",
|
| 322 |
file_types=[".pdf",".txt"], type="filepath", elem_classes=["upload-like"])
|
|
|
|
| 333 |
)
|
| 334 |
trocr_zoom = gr.Slider(2.0, 3.5, value=DEFAULT_TROCR_ZOOM, step=0.1, label="Zoom OCR")
|
| 335 |
btn_build = gr.Button("generate quistion", elem_classes=["button-primary"])
|
| 336 |
+
warn = gr.Markdown("", elem_classes=["small"])
|
| 337 |
|
| 338 |
+
# الصفحة 2
|
| 339 |
page2 = gr.Group(visible=False)
|
| 340 |
with page2:
|
| 341 |
quiz_html = gr.HTML("")
|
| 342 |
|
| 343 |
+
# بناء الامتحان + إظهار الصفحة 2
|
| 344 |
btn_build.click(
|
| 345 |
build_quiz,
|
| 346 |
inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
|
| 347 |
+
outputs=[quiz_html, page1, page2, warn]
|
| 348 |
+
).then( # بعد عرض الـHTML اربط أزرار Submit
|
| 349 |
+
None, inputs=None, outputs=[],
|
| 350 |
+
js=ATTACH_LISTENERS_JS
|
| 351 |
)
|
| 352 |
|
| 353 |
if __name__ == "__main__":
|