Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,71 +1,24 @@
|
|
| 1 |
# =============================================================================
|
| 2 |
-
# INDO-BLOOM
|
| 3 |
#
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
# TAB 2 β LocalQA v1.0: Generate QA pairs C1+C2 dari context (GPU lokal)
|
| 7 |
-
# TAB 3 β Pipeline : Jalankan IBEX β QA sekaligus dari PDF ke corpus
|
| 8 |
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
# Kaggle: aktifkan GPU T4 di Settings β Accelerator
|
| 13 |
# =============================================================================
|
| 14 |
-
|
| 15 |
import subprocess, sys
|
| 16 |
subprocess.run([sys.executable, "-m", "pip", "install", "-q",
|
| 17 |
-
"gradio", "pymupdf", "pandas",
|
| 18 |
-
"transformers", "accelerate"], check=False)
|
| 19 |
|
| 20 |
-
import os, re,
|
| 21 |
import pandas as pd
|
| 22 |
import fitz # PyMuPDF
|
| 23 |
import gradio as gr
|
| 24 |
|
| 25 |
-
# ββ Lazy-load model (hanya saat Tab 2/3 dipakai) ββββββββββββββββββββββββββ
|
| 26 |
-
import torch
|
| 27 |
-
_tokenizer = None
|
| 28 |
-
_model = None
|
| 29 |
-
_device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 30 |
-
MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct"
|
| 31 |
-
|
| 32 |
-
def muat_model():
|
| 33 |
-
global _tokenizer, _model
|
| 34 |
-
if _tokenizer is None:
|
| 35 |
-
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 36 |
-
print(f"π Memuat {MODEL_NAME} ke {_device.upper()}...")
|
| 37 |
-
_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
| 38 |
-
|
| 39 |
-
if _device == "cuda":
|
| 40 |
-
# GPU tersedia β load ke VRAM penuh, fp16
|
| 41 |
-
_model = AutoModelForCausalLM.from_pretrained(
|
| 42 |
-
MODEL_NAME,
|
| 43 |
-
torch_dtype=torch.float16,
|
| 44 |
-
device_map="cuda",
|
| 45 |
-
trust_remote_code=True,
|
| 46 |
-
)
|
| 47 |
-
else:
|
| 48 |
-
# CPU-only (HF Spaces gratis) β JANGAN pakai device_map="auto"
|
| 49 |
-
# device_map="auto" pada CPU menyebabkan layer offload ke 'meta'
|
| 50 |
-
# yang menghasilkan RuntimeError: Tensor on device cpu != meta
|
| 51 |
-
# Solusi: load ke CPU secara eksplisit tanpa device_map
|
| 52 |
-
_model = AutoModelForCausalLM.from_pretrained(
|
| 53 |
-
MODEL_NAME,
|
| 54 |
-
torch_dtype=torch.float32, # fp32 lebih stabil di CPU
|
| 55 |
-
device_map=None, # β KUNCI: tidak pakai auto
|
| 56 |
-
low_cpu_mem_usage=True, # kurangi peak RAM saat loading
|
| 57 |
-
trust_remote_code=True,
|
| 58 |
-
)
|
| 59 |
-
_model = _model.to("cpu") # pastikan semua layer di CPU
|
| 60 |
-
|
| 61 |
-
_model.eval()
|
| 62 |
-
print(f"β
Model siap di {_device.upper()}.")
|
| 63 |
-
return _tokenizer, _model
|
| 64 |
-
|
| 65 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 66 |
-
#
|
| 67 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
-
|
| 69 |
NOISE_BSE = [
|
| 70 |
r'tujuan pembelajaran', r'setelah mempelajari',
|
| 71 |
r'diharapkan (mampu|dapat)', r'kata kunci', r'pemetaan pikiran',
|
|
@@ -109,11 +62,12 @@ KALIMAT_INSTRUKSIONAL = [
|
|
| 109 |
r'kalian\s+telah\s+(mengetahui|mempelajari|memahami)\s+bahwa',
|
| 110 |
r'apa\s+yang\s+dimaksud.{0,80}\?$',
|
| 111 |
]
|
| 112 |
-
KALIMAT_PATTERNS
|
| 113 |
-
HEADER_BAB_PAT
|
| 114 |
-
_MAPEL
|
| 115 |
-
|
| 116 |
-
|
|
|
|
| 117 |
'karena','menyebabkan','berdampak','sehingga','mengakibatkan','berakibat',
|
| 118 |
'oleh karena','disebabkan','bertujuan','berfungsi','berperan','berguna',
|
| 119 |
'tujuan','fungsi','manfaat','peran','kegunaan','mekanisme','tahapan',
|
|
@@ -121,6 +75,9 @@ KATA_KUNCI_C2 = [
|
|
| 121 |
'keterkaitan','mengapa','bagaimana','jelaskan','uraikan',
|
| 122 |
]
|
| 123 |
|
|
|
|
|
|
|
|
|
|
| 124 |
def hitung_noise(t):
|
| 125 |
return sum(1 for p in NOISE_PATTERNS if p.search(t))
|
| 126 |
|
|
@@ -162,10 +119,6 @@ def bersihkan_kalimat(teks):
|
|
| 162 |
|
| 163 |
def ekstrak_context_dari_pdf(pdf_path, hal_mulai, hal_selesai,
|
| 164 |
chunk_size, batas_noise, nama_sumber="BSE"):
|
| 165 |
-
"""
|
| 166 |
-
Core extractor IBEX v2.4.
|
| 167 |
-
Return: (df_chunks, pesan_status, tmp_csv_path)
|
| 168 |
-
"""
|
| 169 |
hal_mulai = int(hal_mulai)
|
| 170 |
hal_selesai = int(hal_selesai)
|
| 171 |
chunk_size = int(chunk_size)
|
|
@@ -180,7 +133,7 @@ def ekstrak_context_dari_pdf(pdf_path, hal_mulai, hal_selesai,
|
|
| 180 |
if start >= end:
|
| 181 |
return None, f"β Rentang tidak valid. PDF punya {total} halaman.", None
|
| 182 |
|
| 183 |
-
# Ekstrak teks
|
| 184 |
teks_per_hal = []
|
| 185 |
for num in range(start, end):
|
| 186 |
page = doc.load_page(num)
|
|
@@ -209,9 +162,9 @@ def ekstrak_context_dari_pdf(pdf_path, hal_mulai, hal_selesai,
|
|
| 209 |
while j < len(kalimat_list) and len(buf) < chunk_size:
|
| 210 |
buf.extend(kalimat_list[j].split())
|
| 211 |
j += 1
|
| 212 |
-
pot
|
| 213 |
-
wc
|
| 214 |
-
noise
|
| 215 |
ada_c2 = any(k in pot.lower() for k in KATA_KUNCI_C2)
|
| 216 |
|
| 217 |
if wc >= 50 and ada_c2:
|
|
@@ -239,163 +192,31 @@ def ekstrak_context_dari_pdf(pdf_path, hal_mulai, hal_selesai,
|
|
| 239 |
if not chunk_meta:
|
| 240 |
return None, (
|
| 241 |
f"β οΈ Tidak ada context C2 bersih.\n"
|
| 242 |
-
f" Dibuang L1: {bng_l1}
|
|
|
|
| 243 |
" Coba perluas halaman atau naikkan toleransi noise."
|
| 244 |
), None
|
| 245 |
|
| 246 |
-
df
|
| 247 |
-
tmp = os.path.join(
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
df.to_csv(tmp, index=False, encoding="utf-8-sig")
|
| 250 |
|
| 251 |
pesan = (
|
| 252 |
-
f"β
{end-start}
|
| 253 |
-
f"π Chunk bersih
|
| 254 |
-
f"ποΈ Buang L1 (
|
| 255 |
-
f"βοΈ Buang L2 (kata)
|
| 256 |
-
f"π Total kata
|
| 257 |
f"πΎ CSV siap diunduh."
|
| 258 |
)
|
| 259 |
return df, pesan, tmp
|
| 260 |
|
| 261 |
-
|
| 262 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 263 |
-
# BAGIAN B β LOCAL QA: FUNGSI GENERATE
|
| 264 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
-
|
| 266 |
-
SYSTEM_MSG = (
|
| 267 |
-
"Anda adalah pakar pembuatan soal Taksonomi Bloom Bahasa Indonesia. "
|
| 268 |
-
"Tugas Anda membuat soal yang tepat sesuai level kognitif yang diminta. "
|
| 269 |
-
"Selalu kembalikan output dalam format JSON yang valid."
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
def prompt_c1(konteks, n):
|
| 273 |
-
return (
|
| 274 |
-
f'Bacalah teks berikut:\n"""{konteks}"""\n\n'
|
| 275 |
-
f"Buat {n} pasang soal-jawaban level C1 (Mengingat).\n"
|
| 276 |
-
"Ketentuan C1:\n"
|
| 277 |
-
"- Pertanyaan diawali: apa, siapa, kapan, di mana, atau berapa\n"
|
| 278 |
-
"- Jawaban berupa fakta eksplisit dari teks (maks 15 kata)\n\n"
|
| 279 |
-
'Output JSON (tanpa teks lain):\n{"c1": [{"question": "...", "answer": "..."}, ...]}'
|
| 280 |
-
)
|
| 281 |
-
|
| 282 |
-
def prompt_c2(konteks, n):
|
| 283 |
-
return (
|
| 284 |
-
f'Bacalah teks berikut:\n"""{konteks}"""\n\n'
|
| 285 |
-
f"Buat {n} pasang soal-jawaban level C2 (Memahami).\n"
|
| 286 |
-
"Ketentuan C2:\n"
|
| 287 |
-
"- Pertanyaan WAJIB diawali: mengapa atau bagaimana\n"
|
| 288 |
-
"- Jawaban menjelaskan sebab-akibat/proses, min 20 kata\n"
|
| 289 |
-
"- Jawaban HARUS mengandung: karena/sehingga/mengakibatkan/berdampak\n"
|
| 290 |
-
"- Jawaban dengan bahasa sendiri, BUKAN copy-paste teks\n\n"
|
| 291 |
-
'Output JSON (tanpa teks lain):\n{"c2": [{"question": "...", "answer": "..."}, ...]}'
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
def generate_json(user_prompt):
|
| 295 |
-
tok, mdl = muat_model()
|
| 296 |
-
messages = [
|
| 297 |
-
{"role": "system", "content": SYSTEM_MSG},
|
| 298 |
-
{"role": "user", "content": user_prompt},
|
| 299 |
-
]
|
| 300 |
-
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 301 |
-
inputs = tok([text], return_tensors="pt").to(_device)
|
| 302 |
-
with torch.no_grad():
|
| 303 |
-
out = mdl.generate(
|
| 304 |
-
**inputs,
|
| 305 |
-
max_new_tokens=600,
|
| 306 |
-
temperature=0.7,
|
| 307 |
-
do_sample=True,
|
| 308 |
-
pad_token_id=tok.eos_token_id,
|
| 309 |
-
)
|
| 310 |
-
gen = out[0][inputs["input_ids"].shape[-1]:]
|
| 311 |
-
raw = tok.decode(gen, skip_special_tokens=True).strip()
|
| 312 |
-
s, e = raw.find('{'), raw.rfind('}') + 1
|
| 313 |
-
if s == -1:
|
| 314 |
-
return None
|
| 315 |
-
try:
|
| 316 |
-
return json.loads(raw[s:e])
|
| 317 |
-
except json.JSONDecodeError:
|
| 318 |
-
cleaned = re.sub(r',\s*([}\]])', r'\1', raw[s:e])
|
| 319 |
-
try:
|
| 320 |
-
return json.loads(cleaned)
|
| 321 |
-
except Exception:
|
| 322 |
-
return None
|
| 323 |
-
|
| 324 |
-
def validasi_c1(q, a):
|
| 325 |
-
if not any(q.lower().startswith(s)
|
| 326 |
-
for s in ["apa","siapa","kapan","di mana","dimana","berapa"]):
|
| 327 |
-
return False, f"Tidak diawali kata tanya C1 ('{q[:30]}')"
|
| 328 |
-
if not (2 <= len(a.split()) <= 20):
|
| 329 |
-
return False, f"Jawaban {len(a.split())} kata (harusnya 2-20)"
|
| 330 |
-
return True, "OK"
|
| 331 |
-
|
| 332 |
-
def validasi_c2(q, a):
|
| 333 |
-
if not any(q.lower().startswith(k) for k in ["mengapa","bagaimana"]):
|
| 334 |
-
return False, f"Tidak diawali 'Mengapa'/'Bagaimana' ('{q[:30]}')"
|
| 335 |
-
if len(a.split()) < 20:
|
| 336 |
-
return False, f"Jawaban terlalu pendek ({len(a.split())} kata)"
|
| 337 |
-
kausal = ['karena','sehingga','mengakibatkan','berdampak',
|
| 338 |
-
'akibatnya','dampaknya','disebabkan','mendorong','menyebabkan']
|
| 339 |
-
if not any(k in a.lower() for k in kausal):
|
| 340 |
-
return False, "Tidak ada penanda kausal"
|
| 341 |
-
return True, "OK"
|
| 342 |
-
|
| 343 |
-
def proses_chunk_qa(chunk_id, konteks, n_c1, n_c2, source_type):
|
| 344 |
-
uid = hashlib.md5(konteks.encode()).hexdigest()[:8]
|
| 345 |
-
valid, errors = [], []
|
| 346 |
-
|
| 347 |
-
prefix = "BSE" if "bse" in source_type.lower() else "WIKI"
|
| 348 |
-
|
| 349 |
-
for level, pfn, vfn, key, atype, label in [
|
| 350 |
-
("C1", prompt_c1, validasi_c1, "c1", "extractive", "Mengingat (Remembering)"),
|
| 351 |
-
("C2", prompt_c2, validasi_c2, "c2", "abstractive", "Memahami (Understanding)"),
|
| 352 |
-
]:
|
| 353 |
-
n = n_c1 if level == "C1" else n_c2
|
| 354 |
-
data = generate_json(pfn(konteks, n))
|
| 355 |
-
if data and key in data:
|
| 356 |
-
for item in data[key]:
|
| 357 |
-
q = item.get("question","").strip()
|
| 358 |
-
a = item.get("answer","").strip()
|
| 359 |
-
ok, alasan = vfn(q, a)
|
| 360 |
-
if ok:
|
| 361 |
-
valid.append({
|
| 362 |
-
"id" : f"{prefix}-{chunk_id}-{uid}-{level}",
|
| 363 |
-
"chunk_id" : chunk_id,
|
| 364 |
-
"source_type" : source_type,
|
| 365 |
-
"bloom_level" : level,
|
| 366 |
-
"bloom_label" : label,
|
| 367 |
-
"answer_type" : atype,
|
| 368 |
-
"question" : q,
|
| 369 |
-
"answer" : a,
|
| 370 |
-
"answer_words": len(a.split()),
|
| 371 |
-
"context" : konteks,
|
| 372 |
-
})
|
| 373 |
-
else:
|
| 374 |
-
errors.append({"chunk_id":chunk_id,"level":level,
|
| 375 |
-
"alasan":alasan,"q":q[:100],"a":a[:100]})
|
| 376 |
-
else:
|
| 377 |
-
errors.append({"chunk_id":chunk_id,"level":level,
|
| 378 |
-
"alasan":f"Gagal parse JSON / key '{key}' tidak ada",
|
| 379 |
-
"q":"","a":""})
|
| 380 |
-
return valid, errors
|
| 381 |
-
|
| 382 |
-
def muat_output_csv(path):
|
| 383 |
-
if not os.path.exists(path) or os.path.getsize(path) == 0:
|
| 384 |
-
return set(), []
|
| 385 |
-
try:
|
| 386 |
-
df = pd.read_csv(path)
|
| 387 |
-
if df.empty or "chunk_id" not in df.columns:
|
| 388 |
-
return set(), []
|
| 389 |
-
print(f"β»οΈ Resume: {len(df)} baris sudah ada.")
|
| 390 |
-
return set(df["chunk_id"].tolist()), df.to_dict("records")
|
| 391 |
-
except Exception:
|
| 392 |
-
return set(), []
|
| 393 |
-
|
| 394 |
-
|
| 395 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 396 |
-
# HANDLER GRADIO
|
| 397 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 398 |
-
|
| 399 |
def handler_ekstrak(file_pdf, hal_mulai, hal_selesai,
|
| 400 |
chunk_size, batas_noise, sumber_tipe):
|
| 401 |
if file_pdf is None:
|
|
@@ -410,388 +231,99 @@ def handler_ekstrak(file_pdf, hal_mulai, hal_selesai,
|
|
| 410 |
)
|
| 411 |
if df is None:
|
| 412 |
return pesan, pd.DataFrame(), None
|
| 413 |
-
|
| 414 |
-
|
|
|
|
| 415 |
except Exception as ex:
|
| 416 |
return f"β Error: {ex}", pd.DataFrame(), None
|
| 417 |
|
| 418 |
-
def
|
| 419 |
-
ada = path is not None and df is not None and not df.empty
|
| 420 |
-
return pesan, df, gr.update(visible=ada, value=path if ada else None)
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
-
# HANDLER GRADIO β TAB 2: QA GENERATOR
|
| 425 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 426 |
-
|
| 427 |
-
def handler_generate_qa(file_csv, n_c1, n_c2, chunk_mulai, chunk_selesai):
|
| 428 |
-
if file_csv is None:
|
| 429 |
-
return "β Harap unggah CSV hasil IBEX.", pd.DataFrame(), None
|
| 430 |
-
|
| 431 |
-
csv_path = file_csv if isinstance(file_csv, str) else file_csv.name
|
| 432 |
-
try:
|
| 433 |
-
df_in = pd.read_csv(csv_path)
|
| 434 |
-
except Exception as ex:
|
| 435 |
-
return f"β Gagal baca CSV: {ex}", pd.DataFrame(), None
|
| 436 |
-
|
| 437 |
-
if "context" not in df_in.columns:
|
| 438 |
-
return "β Kolom 'context' tidak ada. Pastikan file dari IBEX.", pd.DataFrame(), None
|
| 439 |
-
|
| 440 |
-
n_c1, n_c2 = int(n_c1), int(n_c2)
|
| 441 |
-
idx_start = max(0, int(chunk_mulai) - 1)
|
| 442 |
-
idx_end = min(len(df_in), int(chunk_selesai))
|
| 443 |
-
df_proses = df_in.iloc[idx_start:idx_end].reset_index(drop=True)
|
| 444 |
-
|
| 445 |
-
out_path = os.path.join(tempfile.gettempdir(), "IndoBloom_QA_output.csv")
|
| 446 |
-
err_path = os.path.join(tempfile.gettempdir(), "IndoBloom_QA_errors.csv")
|
| 447 |
-
processed_ids, all_rows = muat_output_csv(out_path)
|
| 448 |
-
err_rows, logs = [], []
|
| 449 |
-
|
| 450 |
-
sisa = len(df_proses) - sum(1 for r in df_proses.itertuples()
|
| 451 |
-
if str(r.chunk_id) in processed_ids)
|
| 452 |
-
logs.append(f"β±οΈ {sisa} chunk akan diproses (~{sisa*30//60} menit di GPU T4)")
|
| 453 |
-
|
| 454 |
-
for idx, row in df_proses.iterrows():
|
| 455 |
-
chunk_id = str(row["chunk_id"])
|
| 456 |
-
konteks = str(row["context"])
|
| 457 |
-
source_type = str(row.get("source_type", "BSE"))
|
| 458 |
-
|
| 459 |
-
if chunk_id in processed_ids:
|
| 460 |
-
logs.append(f"[{idx+1}] {chunk_id} β dilewati.")
|
| 461 |
-
continue
|
| 462 |
-
|
| 463 |
-
logs.append(f"[{idx+1}/{len(df_proses)}] {chunk_id}...")
|
| 464 |
-
t0 = time.time()
|
| 465 |
-
valid, errors = proses_chunk_qa(chunk_id, konteks, n_c1, n_c2, source_type)
|
| 466 |
-
elapsed = time.time() - t0
|
| 467 |
-
|
| 468 |
-
all_rows.extend(valid)
|
| 469 |
-
err_rows.extend(errors)
|
| 470 |
-
processed_ids.add(chunk_id)
|
| 471 |
-
|
| 472 |
-
c1ok = sum(1 for r in valid if r["bloom_level"]=="C1")
|
| 473 |
-
c2ok = sum(1 for r in valid if r["bloom_level"]=="C2")
|
| 474 |
-
c1er = sum(1 for e in errors if e["level"]=="C1")
|
| 475 |
-
c2er = sum(1 for e in errors if e["level"]=="C2")
|
| 476 |
-
logs.append(f" β
C1:{c1ok}(β{c1er}) C2:{c2ok}(β{c2er}) | {elapsed:.0f}s")
|
| 477 |
-
for r in valid:
|
| 478 |
-
logs.append(f" [{r['bloom_level']}] Q: {r['question'][:70]}")
|
| 479 |
-
logs.append(f" A: {r['answer'][:70]}")
|
| 480 |
-
|
| 481 |
-
if len(all_rows) % 5 == 0 and all_rows:
|
| 482 |
-
pd.DataFrame(all_rows).to_csv(out_path, index=False, encoding="utf-8-sig")
|
| 483 |
-
logs.append(f" πΎ Checkpoint: {len(all_rows)} QA.")
|
| 484 |
-
|
| 485 |
-
if not all_rows:
|
| 486 |
-
return "β Tidak ada QA yang berhasil digenerate.", pd.DataFrame(), None
|
| 487 |
-
|
| 488 |
-
df_out = pd.DataFrame(all_rows)
|
| 489 |
-
df_out.to_csv(out_path, index=False, encoding="utf-8-sig")
|
| 490 |
-
if err_rows:
|
| 491 |
-
pd.DataFrame(err_rows).to_csv(err_path, index=False, encoding="utf-8-sig")
|
| 492 |
-
|
| 493 |
-
c1t = len(df_out[df_out["bloom_level"]=="C1"])
|
| 494 |
-
c2t = len(df_out[df_out["bloom_level"]=="C2"])
|
| 495 |
-
pesan = "\n".join([
|
| 496 |
-
f"β
SELESAI β {len(df_out)} QA dihasilkan",
|
| 497 |
-
f" C1: {c1t} | C2: {c2t}",
|
| 498 |
-
f" Rata-rata jawaban: {df_out['answer_words'].mean():.1f} kata",
|
| 499 |
-
f" Error: {len(err_rows)}",
|
| 500 |
-
"β" * 40,
|
| 501 |
-
*logs[-30:], # tampilkan 30 log terakhir
|
| 502 |
-
])
|
| 503 |
-
preview = df_out[["chunk_id","bloom_level","question","answer"]].head(20)
|
| 504 |
-
return pesan, preview, out_path
|
| 505 |
-
|
| 506 |
-
def handler_unduh_qa(pesan, df, path):
|
| 507 |
ada = path is not None and df is not None and not df.empty
|
| 508 |
return pesan, df, gr.update(visible=ada, value=path if ada else None)
|
| 509 |
|
| 510 |
-
|
| 511 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 512 |
-
# HANDLER GRADIO β TAB 3: PIPELINE (PDF β CONTEXT β QA)
|
| 513 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 514 |
-
|
| 515 |
-
def handler_pipeline(file_pdf, hal_mulai, hal_selesai, chunk_size,
|
| 516 |
-
batas_noise, sumber_tipe, n_c1, n_c2):
|
| 517 |
-
if file_pdf is None:
|
| 518 |
-
return "β Harap unggah file PDF.", pd.DataFrame(), None
|
| 519 |
-
|
| 520 |
-
logs = ["π PIPELINE DIMULAI", "β"*40]
|
| 521 |
-
|
| 522 |
-
# STEP 1: Ekstrak context
|
| 523 |
-
logs.append("π STEP 1: Ekstrak context dari PDF...")
|
| 524 |
-
try:
|
| 525 |
-
pdf_path = file_pdf if isinstance(file_pdf, str) else file_pdf.name
|
| 526 |
-
df_ctx, pesan_ibex, _ = ekstrak_context_dari_pdf(
|
| 527 |
-
pdf_path, hal_mulai, hal_selesai,
|
| 528 |
-
chunk_size, batas_noise, sumber_tipe
|
| 529 |
-
)
|
| 530 |
-
except Exception as ex:
|
| 531 |
-
return f"β Gagal ekstrak: {ex}", pd.DataFrame(), None
|
| 532 |
-
|
| 533 |
-
if df_ctx is None:
|
| 534 |
-
return f"β Ekstrak gagal:\n{pesan_ibex}", pd.DataFrame(), None
|
| 535 |
-
|
| 536 |
-
logs.append(pesan_ibex)
|
| 537 |
-
logs.append(f"β
{len(df_ctx)} chunk siap di-generate")
|
| 538 |
-
logs.append("β"*40)
|
| 539 |
-
|
| 540 |
-
# STEP 2: Generate QA
|
| 541 |
-
logs.append("π§ STEP 2: Generate QA pairs (C1+C2)...")
|
| 542 |
-
all_rows, err_rows = [], []
|
| 543 |
-
n_c1, n_c2 = int(n_c1), int(n_c2)
|
| 544 |
-
|
| 545 |
-
for idx, row in df_ctx.iterrows():
|
| 546 |
-
chunk_id = str(row["chunk_id"])
|
| 547 |
-
konteks = str(row["context"])
|
| 548 |
-
source_type = str(row.get("source_type", sumber_tipe))
|
| 549 |
-
|
| 550 |
-
logs.append(f"[{idx+1}/{len(df_ctx)}] {chunk_id}...")
|
| 551 |
-
t0 = time.time()
|
| 552 |
-
valid, errors = proses_chunk_qa(chunk_id, konteks, n_c1, n_c2, source_type)
|
| 553 |
-
elapsed = time.time() - t0
|
| 554 |
-
|
| 555 |
-
all_rows.extend(valid)
|
| 556 |
-
err_rows.extend(errors)
|
| 557 |
-
|
| 558 |
-
c1ok = sum(1 for r in valid if r["bloom_level"]=="C1")
|
| 559 |
-
c2ok = sum(1 for r in valid if r["bloom_level"]=="C2")
|
| 560 |
-
logs.append(f" β
C1:{c1ok} C2:{c2ok} | {elapsed:.0f}s")
|
| 561 |
-
for r in valid:
|
| 562 |
-
logs.append(f" [{r['bloom_level']}] {r['question'][:65]}")
|
| 563 |
-
|
| 564 |
-
if not all_rows:
|
| 565 |
-
return "β Tidak ada QA berhasil.", pd.DataFrame(), None
|
| 566 |
-
|
| 567 |
-
df_out = pd.DataFrame(all_rows)
|
| 568 |
-
out_path = os.path.join(
|
| 569 |
-
tempfile.gettempdir(),
|
| 570 |
-
f"IndoBloom_Pipeline_{os.path.basename(pdf_path).replace('.pdf','')}.csv"
|
| 571 |
-
)
|
| 572 |
-
df_out.to_csv(out_path, index=False, encoding="utf-8-sig")
|
| 573 |
-
|
| 574 |
-
c1t = len(df_out[df_out["bloom_level"]=="C1"])
|
| 575 |
-
c2t = len(df_out[df_out["bloom_level"]=="C2"])
|
| 576 |
-
logs += [
|
| 577 |
-
"β"*40,
|
| 578 |
-
f"β
PIPELINE SELESAI",
|
| 579 |
-
f" Total QA : {len(df_out)} pairs",
|
| 580 |
-
f" C1 : {c1t} | C2: {c2t}",
|
| 581 |
-
f" Error : {len(err_rows)}",
|
| 582 |
-
f" Output : {out_path}",
|
| 583 |
-
]
|
| 584 |
-
pesan = "\n".join(logs)
|
| 585 |
-
preview = df_out[["chunk_id","bloom_level","question","answer"]].head(20)
|
| 586 |
-
return pesan, preview, out_path
|
| 587 |
-
|
| 588 |
-
def handler_unduh_pipeline(pesan, df, path):
|
| 589 |
-
ada = path is not None and df is not None and not df.empty
|
| 590 |
-
return pesan, df, gr.update(visible=ada, value=path if ada else None)
|
| 591 |
-
|
| 592 |
-
|
| 593 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 594 |
-
#
|
| 595 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 596 |
-
|
| 597 |
-
|
| 598 |
|
| 599 |
gr.Markdown("""
|
| 600 |
-
#
|
| 601 |
-
|
|
|
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
""")
|
| 606 |
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 615 |
-
# TAB 1 β IBEX EXTRACTOR
|
| 616 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 617 |
-
with gr.TabItem("π Tab 1 β IBEX: Ekstrak Context"):
|
| 618 |
-
gr.Markdown("""
|
| 619 |
-
### IBEX v2.4 β Indo-Bloom Context Extractor
|
| 620 |
-
Ekstrak teks eksplanatori (C2-ready) dari **PDF BSE atau Wikipedia**.
|
| 621 |
-
Filter 2 level: buang noise BSE + bersihkan kalimat instruksional.
|
| 622 |
-
""")
|
| 623 |
-
with gr.Row():
|
| 624 |
-
with gr.Column(scale=1):
|
| 625 |
-
t1_pdf = gr.File(label="π Upload PDF", file_types=[".pdf"])
|
| 626 |
-
t1_sumber = gr.Radio(
|
| 627 |
-
choices=["BSE", "Wikipedia", "Lainnya"],
|
| 628 |
-
value="BSE", label="Tipe Sumber"
|
| 629 |
-
)
|
| 630 |
-
with gr.Row():
|
| 631 |
-
t1_hal1 = gr.Number(label="Hal. Mulai", value=1, precision=0, minimum=1)
|
| 632 |
-
t1_hal2 = gr.Number(label="Hal. Selesai", value=20, precision=0, minimum=1)
|
| 633 |
-
t1_chunk = gr.Slider(50, 300, value=150, step=10,
|
| 634 |
-
label="Kata per Chunk",
|
| 635 |
-
info="Rekomendasi 100β200. Overlap 25% otomatis.")
|
| 636 |
-
t1_noise = gr.Slider(1, 6, value=2, step=1,
|
| 637 |
-
label="Toleransi Noise",
|
| 638 |
-
info="1β2=ketat | 3β4=sedang | 5β6=longgar")
|
| 639 |
-
t1_btn = gr.Button("π Ekstrak Context", variant="primary", size="lg")
|
| 640 |
-
|
| 641 |
-
with gr.Column(scale=2):
|
| 642 |
-
t1_status = gr.Textbox(label="Status", lines=7, interactive=False)
|
| 643 |
-
t1_unduh = gr.DownloadButton("β¬οΈ Unduh CSV Context",
|
| 644 |
-
variant="secondary", visible=False)
|
| 645 |
-
t1_tabel = gr.Dataframe(label="Preview Chunks", interactive=False, wrap=True)
|
| 646 |
-
|
| 647 |
-
t1_btn.click(
|
| 648 |
-
fn=handler_ekstrak,
|
| 649 |
-
inputs=[t1_pdf, t1_hal1, t1_hal2, t1_chunk, t1_noise, t1_sumber],
|
| 650 |
-
outputs=[t1_status, t1_tabel, ibex_csv_state]
|
| 651 |
-
).then(
|
| 652 |
-
fn=handler_unduh_ibex,
|
| 653 |
-
inputs=[t1_status, t1_tabel, ibex_csv_state],
|
| 654 |
-
outputs=[t1_status, t1_tabel, t1_unduh]
|
| 655 |
)
|
| 656 |
-
|
| 657 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 658 |
-
# TAB 2 β QA GENERATOR
|
| 659 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 660 |
-
with gr.TabItem("π§ Tab 2 β QA Generator (GPU Lokal)"):
|
| 661 |
-
gr.Markdown("""
|
| 662 |
-
### Local QA Generator v1.0
|
| 663 |
-
Generate pasangan **Q&A level C1 + C2** dari CSV context hasil IBEX.
|
| 664 |
-
Model: **Qwen2.5-3B-Instruct** β lokal, tanpa API key, tanpa rate limit.
|
| 665 |
-
> β οΈ Aktifkan **GPU T4** di Kaggle: Settings β Accelerator β GPU T4
|
| 666 |
-
""")
|
| 667 |
with gr.Row():
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
gr.Markdown("*~30s/chunk di GPU T4. Resume otomatis jika terganggu.*")
|
| 678 |
-
|
| 679 |
-
with gr.Column(scale=2):
|
| 680 |
-
t2_status = gr.Textbox(label="Log & Status", lines=12, interactive=False)
|
| 681 |
-
t2_unduh = gr.DownloadButton("β¬οΈ Unduh CSV QA Corpus",
|
| 682 |
-
variant="secondary", visible=False)
|
| 683 |
-
t2_tabel = gr.Dataframe(label="Preview QA Pairs", interactive=False, wrap=True)
|
| 684 |
-
|
| 685 |
-
t2_btn.click(
|
| 686 |
-
fn=handler_generate_qa,
|
| 687 |
-
inputs=[t2_csv, t2_c1, t2_c2, t2_cm, t2_cs],
|
| 688 |
-
outputs=[t2_status, t2_tabel, qa_csv_state]
|
| 689 |
-
).then(
|
| 690 |
-
fn=handler_unduh_qa,
|
| 691 |
-
inputs=[t2_status, t2_tabel, qa_csv_state],
|
| 692 |
-
outputs=[t2_status, t2_tabel, t2_unduh]
|
| 693 |
-
)
|
| 694 |
|
| 695 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 696 |
-
# TAB 3 β FULL PIPELINE
|
| 697 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 698 |
-
with gr.TabItem("β‘ Tab 3 β Full Pipeline (PDF β QA)"):
|
| 699 |
gr.Markdown("""
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
""")
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
t3_hal2 = gr.Number(label="Hal. Selesai", value=20, precision=0, minimum=1)
|
| 715 |
-
t3_chunk = gr.Slider(50, 300, value=150, step=10, label="Kata per Chunk")
|
| 716 |
-
t3_noise = gr.Slider(1, 6, value=2, step=1, label="Toleransi Noise")
|
| 717 |
-
gr.Markdown("**π§ Pengaturan Generate**")
|
| 718 |
-
with gr.Row():
|
| 719 |
-
t3_c1 = gr.Slider(1, 5, value=2, step=1, label="QA C1 per Chunk")
|
| 720 |
-
t3_c2 = gr.Slider(1, 5, value=2, step=1, label="QA C2 per Chunk")
|
| 721 |
-
t3_btn = gr.Button("β‘ Jalankan Full Pipeline",
|
| 722 |
-
variant="primary", size="lg")
|
| 723 |
-
|
| 724 |
-
with gr.Column(scale=2):
|
| 725 |
-
t3_status = gr.Textbox(label="Log Pipeline", lines=14, interactive=False)
|
| 726 |
-
t3_unduh = gr.DownloadButton("β¬οΈ Unduh QA Corpus Final",
|
| 727 |
-
variant="secondary", visible=False)
|
| 728 |
-
t3_tabel = gr.Dataframe(label="Preview Hasil", interactive=False, wrap=True)
|
| 729 |
-
|
| 730 |
-
t3_btn.click(
|
| 731 |
-
fn=handler_pipeline,
|
| 732 |
-
inputs=[t3_pdf, t3_hal1, t3_hal2, t3_chunk,
|
| 733 |
-
t3_noise, t3_sumber, t3_c1, t3_c2],
|
| 734 |
-
outputs=[t3_status, t3_tabel, pipeline_csv_state]
|
| 735 |
-
).then(
|
| 736 |
-
fn=handler_unduh_pipeline,
|
| 737 |
-
inputs=[t3_status, t3_tabel, pipeline_csv_state],
|
| 738 |
-
outputs=[t3_status, t3_tabel, t3_unduh]
|
| 739 |
)
|
| 740 |
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
| `chunk_id` | ID chunk dari IBEX |
|
| 766 |
-
| `source_type` | BSE / Wikipedia / Lainnya |
|
| 767 |
-
| `bloom_level` | C1 atau C2 |
|
| 768 |
-
| `bloom_label` | Label lengkap Bloom |
|
| 769 |
-
| `answer_type` | extractive (C1) / abstractive (C2) |
|
| 770 |
-
| `question` | Pertanyaan Bahasa Indonesia |
|
| 771 |
-
| `answer` | Jawaban berdasarkan konteks |
|
| 772 |
-
| `answer_words` | Jumlah kata jawaban |
|
| 773 |
-
| `context` | Teks sumber (untuk validasi) |
|
| 774 |
-
|
| 775 |
-
## ποΈ Tips Penggunaan
|
| 776 |
-
|
| 777 |
-
**Untuk BSE (Buku Sekolah Elektronik):**
|
| 778 |
-
- Toleransi Noise: **2** (default) β ketat untuk menyaring instruksi siswa
|
| 779 |
-
- Chunk size: **150 kata** β cukup untuk context C2
|
| 780 |
-
|
| 781 |
-
**Untuk Wikipedia (PDF):**
|
| 782 |
-
- Toleransi Noise: **4** β Wikipedia lebih bersih, bisa lebih longgar
|
| 783 |
-
- Chunk size: **200 kata** β paragraf Wikipedia biasanya lebih panjang
|
| 784 |
-
- Pilih **"Wikipedia"** di Tipe Sumber agar ID corpus tercatat benar
|
| 785 |
-
|
| 786 |
-
## β οΈ Catatan GPU
|
| 787 |
-
Model Qwen2.5-3B-Instruct membutuhkan GPU untuk kecepatan optimal.
|
| 788 |
-
Di Kaggle: **Settings β Accelerator β GPU T4** sebelum menjalankan Tab 2/3.
|
| 789 |
-
Di CPU: tetap berjalan tapi ~5-10x lebih lambat per chunk.
|
| 790 |
-
""")
|
| 791 |
|
| 792 |
if __name__ == "__main__":
|
| 793 |
app.launch(
|
| 794 |
share=False,
|
| 795 |
theme=gr.themes.Soft(primary_hue="teal"),
|
| 796 |
-
css=".tab-nav button { font-size: 15px; font-weight: 600; }"
|
| 797 |
)
|
|
|
|
| 1 |
# =============================================================================
|
| 2 |
+
# INDO-BLOOM SPACE 1 β IBEX v2.4: Context Extractor
|
| 3 |
#
|
| 4 |
+
# Ekstrak context bersih (C2-ready) dari PDF BSE / Wikipedia.
|
| 5 |
+
# Output: CSV yang bisa diunduh β upload ke Space 2 untuk generate QA.
|
|
|
|
|
|
|
| 6 |
#
|
| 7 |
+
# Hardware: CPU (tidak butuh GPU)
|
| 8 |
+
# Requirements: gradio pymupdf pandas
|
|
|
|
|
|
|
| 9 |
# =============================================================================
|
|
|
|
| 10 |
import subprocess, sys
|
| 11 |
subprocess.run([sys.executable, "-m", "pip", "install", "-q",
|
| 12 |
+
"gradio", "pymupdf", "pandas"], check=False)
|
|
|
|
| 13 |
|
| 14 |
+
import os, re, tempfile
|
| 15 |
import pandas as pd
|
| 16 |
import fitz # PyMuPDF
|
| 17 |
import gradio as gr
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
# KONSTANTA & POLA FILTER
|
| 21 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 22 |
NOISE_BSE = [
|
| 23 |
r'tujuan pembelajaran', r'setelah mempelajari',
|
| 24 |
r'diharapkan (mampu|dapat)', r'kata kunci', r'pemetaan pikiran',
|
|
|
|
| 62 |
r'kalian\s+telah\s+(mengetahui|mempelajari|memahami)\s+bahwa',
|
| 63 |
r'apa\s+yang\s+dimaksud.{0,80}\?$',
|
| 64 |
]
|
| 65 |
+
KALIMAT_PATTERNS = [re.compile(p, re.IGNORECASE) for p in KALIMAT_INSTRUKSIONAL]
|
| 66 |
+
HEADER_BAB_PAT = re.compile(r'bab\s+\d+\s*\|\s*[\w\s]+\d+', re.IGNORECASE)
|
| 67 |
+
_MAPEL = (r'sosiologi|matematika|fisika|kimia|biologi|sejarah|geografi|'
|
| 68 |
+
r'ekonomi|pkn|pendidikan|prakarya|seni|bahasa')
|
| 69 |
+
|
| 70 |
+
KATA_KUNCI_C2 = [
|
| 71 |
'karena','menyebabkan','berdampak','sehingga','mengakibatkan','berakibat',
|
| 72 |
'oleh karena','disebabkan','bertujuan','berfungsi','berperan','berguna',
|
| 73 |
'tujuan','fungsi','manfaat','peran','kegunaan','mekanisme','tahapan',
|
|
|
|
| 75 |
'keterkaitan','mengapa','bagaimana','jelaskan','uraikan',
|
| 76 |
]
|
| 77 |
|
| 78 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββββββ
|
| 79 |
+
# FUNGSI INTI IBEX
|
| 80 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 81 |
def hitung_noise(t):
|
| 82 |
return sum(1 for p in NOISE_PATTERNS if p.search(t))
|
| 83 |
|
|
|
|
| 119 |
|
| 120 |
def ekstrak_context_dari_pdf(pdf_path, hal_mulai, hal_selesai,
|
| 121 |
chunk_size, batas_noise, nama_sumber="BSE"):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
hal_mulai = int(hal_mulai)
|
| 123 |
hal_selesai = int(hal_selesai)
|
| 124 |
chunk_size = int(chunk_size)
|
|
|
|
| 133 |
if start >= end:
|
| 134 |
return None, f"β Rentang tidak valid. PDF punya {total} halaman.", None
|
| 135 |
|
| 136 |
+
# Ekstrak teks per halaman
|
| 137 |
teks_per_hal = []
|
| 138 |
for num in range(start, end):
|
| 139 |
page = doc.load_page(num)
|
|
|
|
| 162 |
while j < len(kalimat_list) and len(buf) < chunk_size:
|
| 163 |
buf.extend(kalimat_list[j].split())
|
| 164 |
j += 1
|
| 165 |
+
pot = " ".join(buf)
|
| 166 |
+
wc = len(buf)
|
| 167 |
+
noise = hitung_noise(pot)
|
| 168 |
ada_c2 = any(k in pot.lower() for k in KATA_KUNCI_C2)
|
| 169 |
|
| 170 |
if wc >= 50 and ada_c2:
|
|
|
|
| 192 |
if not chunk_meta:
|
| 193 |
return None, (
|
| 194 |
f"β οΈ Tidak ada context C2 bersih.\n"
|
| 195 |
+
f" Dibuang L1 (chunk): {bng_l1}\n"
|
| 196 |
+
f" Dibuang L2 (kata) : {bng_pendek}\n"
|
| 197 |
" Coba perluas halaman atau naikkan toleransi noise."
|
| 198 |
), None
|
| 199 |
|
| 200 |
+
df = pd.DataFrame(chunk_meta)
|
| 201 |
+
tmp = os.path.join(
|
| 202 |
+
tempfile.gettempdir(),
|
| 203 |
+
f"IBEX_{nama.replace('.pdf','')}_hal{hal_mulai}-{hal_selesai}.csv"
|
| 204 |
+
)
|
| 205 |
df.to_csv(tmp, index=False, encoding="utf-8-sig")
|
| 206 |
|
| 207 |
pesan = (
|
| 208 |
+
f"β
Selesai! {end - start} halaman dari '{nama}'\n"
|
| 209 |
+
f"π Chunk bersih : {len(chunk_meta)}\n"
|
| 210 |
+
f"ποΈ Buang L1 (noise) : {bng_l1}\n"
|
| 211 |
+
f"βοΈ Buang L2 (kata) : {sum(r['kata_dibuang_l2'] for r in chunk_meta)}\n"
|
| 212 |
+
f"π Total kata input : {len(teks_gabung.split()):,}\n"
|
| 213 |
f"πΎ CSV siap diunduh."
|
| 214 |
)
|
| 215 |
return df, pesan, tmp
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
+
# HANDLER GRADIO
|
| 219 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 220 |
def handler_ekstrak(file_pdf, hal_mulai, hal_selesai,
|
| 221 |
chunk_size, batas_noise, sumber_tipe):
|
| 222 |
if file_pdf is None:
|
|
|
|
| 231 |
)
|
| 232 |
if df is None:
|
| 233 |
return pesan, pd.DataFrame(), None
|
| 234 |
+
preview = df[["chunk_id", "source_type", "page_range",
|
| 235 |
+
"word_count", "noise_score", "context"]]
|
| 236 |
+
return pesan, preview, tmp
|
| 237 |
except Exception as ex:
|
| 238 |
return f"β Error: {ex}", pd.DataFrame(), None
|
| 239 |
|
| 240 |
+
def handler_unduh(pesan, df, path):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
ada = path is not None and df is not None and not df.empty
|
| 242 |
return pesan, df, gr.update(visible=ada, value=path if ada else None)
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 245 |
+
# UI GRADIO
|
| 246 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 247 |
+
with gr.Blocks(title="IBEX v2.4 β Indo-Bloom Context Extractor") as app:
|
| 248 |
+
csv_state = gr.State(None)
|
| 249 |
|
| 250 |
gr.Markdown("""
|
| 251 |
+
# π IBEX v2.4 β Indo-Bloom Context Extractor
|
| 252 |
+
Ekstrak teks eksplanatori **(C2-ready)** dari PDF BSE Kemendikbud atau Wikipedia.
|
| 253 |
+
Filter 2 level: buang chunk noise β bersihkan kalimat instruksional per kalimat.
|
| 254 |
|
| 255 |
+
**Alur kerja:**
|
| 256 |
+
`Upload PDF` β `Atur parameter` β `Ekstrak` β `Unduh CSV` β *upload ke Space QA Generator*
|
| 257 |
""")
|
| 258 |
|
| 259 |
+
with gr.Row():
|
| 260 |
+
# ββ Kolom kiri: kontrol ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 261 |
+
with gr.Column(scale=1):
|
| 262 |
+
pdf_input = gr.File(label="π Upload PDF (BSE / Wikipedia)", file_types=[".pdf"])
|
| 263 |
+
sumber_tipe = gr.Radio(
|
| 264 |
+
choices=["BSE", "Wikipedia", "Lainnya"],
|
| 265 |
+
value="BSE", label="Tipe Sumber"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
with gr.Row():
|
| 268 |
+
hal_mulai = gr.Number(label="Hal. Mulai", value=1, precision=0, minimum=1)
|
| 269 |
+
hal_selesai = gr.Number(label="Hal. Selesai", value=20, precision=0, minimum=1)
|
| 270 |
+
chunk_size = gr.Slider(50, 300, value=150, step=10,
|
| 271 |
+
label="Kata per Chunk",
|
| 272 |
+
info="Rekomendasi 100β200. Overlap 25% otomatis.")
|
| 273 |
+
batas_noise = gr.Slider(1, 6, value=2, step=1,
|
| 274 |
+
label="Toleransi Noise",
|
| 275 |
+
info="1β2=ketat (BSE) | 3β4=sedang | 5β6=longgar (Wiki)")
|
| 276 |
+
btn_ekstrak = gr.Button("π Ekstrak Context", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
gr.Markdown("""
|
| 279 |
+
---
|
| 280 |
+
### π‘ Tips
|
| 281 |
+
| Sumber | Noise | Chunk |
|
| 282 |
+
|--------|-------|-------|
|
| 283 |
+
| BSE Kemendikbud | 2 | 150 |
|
| 284 |
+
| Wikipedia PDF | 4 | 200 |
|
| 285 |
+
|
| 286 |
+
Setelah unduh CSV, upload ke **Space QA Generator** untuk generate soal C1+C2.
|
| 287 |
""")
|
| 288 |
+
|
| 289 |
+
# ββ Kolom kanan: output ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 290 |
+
with gr.Column(scale=2):
|
| 291 |
+
status_box = gr.Textbox(label="Status Ekstraksi", lines=8, interactive=False)
|
| 292 |
+
unduh_btn = gr.DownloadButton(
|
| 293 |
+
"β¬οΈ Unduh CSV Context (upload ke Space QA Generator)",
|
| 294 |
+
variant="secondary", visible=False
|
| 295 |
+
)
|
| 296 |
+
preview = gr.Dataframe(
|
| 297 |
+
label="Preview Chunks (10 pertama)", interactive=False, wrap=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
)
|
| 299 |
|
| 300 |
+
btn_ekstrak.click(
|
| 301 |
+
fn=handler_ekstrak,
|
| 302 |
+
inputs=[pdf_input, hal_mulai, hal_selesai, chunk_size, batas_noise, sumber_tipe],
|
| 303 |
+
outputs=[status_box, preview, csv_state]
|
| 304 |
+
).then(
|
| 305 |
+
fn=handler_unduh,
|
| 306 |
+
inputs=[status_box, preview, csv_state],
|
| 307 |
+
outputs=[status_box, preview, unduh_btn]
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
gr.Markdown("""
|
| 311 |
+
---
|
| 312 |
+
### π Format Kolom Output CSV
|
| 313 |
+
| Kolom | Keterangan |
|
| 314 |
+
|---|---|
|
| 315 |
+
| `chunk_id` | ID unik chunk (chunk_0001, dst.) |
|
| 316 |
+
| `source_file` | Nama file PDF asal |
|
| 317 |
+
| `source_type` | BSE / Wikipedia / Lainnya |
|
| 318 |
+
| `page_range` | Rentang halaman yang diekstrak |
|
| 319 |
+
| `word_count` | Jumlah kata setelah dibersihkan |
|
| 320 |
+
| `noise_score` | Skor noise akhir (idealnya < batas) |
|
| 321 |
+
| `kata_dibuang_l2` | Kata yang dibuang di filter L2 |
|
| 322 |
+
| `context` | Teks context bersih siap di-generate |
|
| 323 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
if __name__ == "__main__":
|
| 326 |
app.launch(
|
| 327 |
share=False,
|
| 328 |
theme=gr.themes.Soft(primary_hue="teal"),
|
|
|
|
| 329 |
)
|