sofhiaazzhr's picture
[NOTICKET] feat(knowledge-parsing): document parsing half of the knowledge pipeline
5840d20
Raw
History Blame
5.83 kB
"""Pemeriksaan mutu hasil parse — menangkap kerusakan yang TIDAK bersuara.
Dasarnya temuan di `Hasil-Uji-MinerU.md`: formula EOQ hasil pipeline CPU
angkanya hilang tanpa error, tanpa peringatan. Kerusakan seperti itu tidak akan
pernah ketahuan dari status "selesai" — harus diperiksa sendiri.
Semua temuan di sini sifatnya PERINGATAN, bukan kegagalan: dokumen tetap
diproses, tapi peringatannya tercatat di manifest supaya tidak ada mutu yang
turun diam-diam di antara ratusan dokumen.
"""
from __future__ import annotations
import re
from collections import Counter
from pathlib import Path
from typing import Any
# Angka di dalam latex. Formula teknis hampir selalu punya angka atau pembagi.
_ADA_ANGKA = re.compile(r"\d")
# Panjang minimal deret angka yang dicocokkan ke sumber.
#
# Diuji ke dokumen nyata: dengan ambang 2 digit, bug asli (55600) tertangkap
# tapi ikut muncul 6 peringatan palsu dari angka pendek — lapisan teks PDF-nya
# memang rusak sebagian, jadi angka pendek sering tidak ketemu padahal benar.
# Dengan 3 digit, bug tetap tertangkap dan derau hilang.
MIN_DIGIT = 3
_SEMUA_ANGKA = re.compile(r"\d+")
def _deret(teks: str, min_digit: int) -> set[str]:
return {a for a in _SEMUA_ANGKA.findall(teks) if len(a) >= min_digit}
def _angka_dari_latex(latex: str, min_digit: int = MIN_DIGIT) -> set[str]:
"""MinerU menulis latex berspasi ('5 5 6 0 0'), jadi spasi dibuang dulu."""
return _deret(re.sub(r"\s+", "", latex), min_digit)
def _angka_per_halaman(pdf: Path, min_digit: int = MIN_DIGIT) -> dict[int, set[str]] | None:
"""Deret angka pada lapisan teks PDF asli, per halaman.
Dipakai sebagai pembanding. Kalau PDF tidak terbaca, kembalikan None dan
pemeriksaan ini dilewati diam-diam — lebih baik tidak memeriksa daripada
memberi peringatan palsu.
"""
try:
from pypdf import PdfReader
reader = PdfReader(str(pdf))
except Exception:
return None
hasil: dict[int, set[str]] = {}
for i, hal in enumerate(reader.pages):
try:
teks = hal.extract_text() or ""
except Exception:
teks = ""
# buang pemisah ribuan & spasi supaya "$5,600" dan "5 600" -> "5600"
bersih = re.sub(r"[, \s]", "", teks)
hasil[i] = _deret(bersih, min_digit)
return hasil
def periksa_items(
items: list[dict[str, Any]],
min_latex_len: int = 8,
pdf_sumber: Path | None = None,
offset_halaman: int = 0,
) -> dict[str, Any]:
"""Kembalikan ringkasan + daftar peringatan untuk satu dokumen.
`pdf_sumber` mengaktifkan pencocokan angka ke PDF asli — satu-satunya cara
menangkap angka yang berubah diam-diam (bug EOQ: 5600 -> 55600).
"""
per_tipe = Counter(x.get("type") for x in items)
halaman = {x.get("page_idx") for x in items if x.get("page_idx") is not None}
peringatan: list[dict[str, Any]] = []
# 1. Halaman tanpa teks sama sekali -> kemungkinan gagal dibaca
teks_per_halaman = Counter(
x.get("page_idx") for x in items
if x.get("type") in {"text", "table", "equation"} and (x.get("text") or x.get("table_body"))
)
for p in sorted(halaman):
if teks_per_halaman.get(p, 0) == 0:
peringatan.append({"jenis": "halaman_kosong", "page": p})
# 2. Formula terpotong / kehilangan angka <- bug EOQ
for i, x in enumerate(items):
if x.get("type") != "equation":
continue
latex = (x.get("text") or "").strip()
if len(latex) < min_latex_len:
peringatan.append({"jenis": "latex_terlalu_pendek", "item": i,
"page": x.get("page_idx"), "isi": latex[:60]})
elif not _ADA_ANGKA.search(latex) and "\\frac" not in latex:
peringatan.append({"jenis": "latex_tanpa_angka", "item": i,
"page": x.get("page_idx"), "isi": latex[:60]})
# 3. ⭐ Angka di formula tidak ada di PDF asli -> berubah diam-diam.
# Ini SATU-SATUNYA pemeriksaan yang bisa menangkap bug EOQ; pemeriksaan
# lain di berkas ini hanya melihat output sendiri, dan angka yang salah
# tetap menghasilkan latex yang sah sepenuhnya.
angka_asli = _angka_per_halaman(pdf_sumber) if pdf_sumber else None
if angka_asli is not None:
for i, x in enumerate(items):
if x.get("type") != "equation":
continue
p = x.get("page_idx", 0) + offset_halaman
acuan = angka_asli.get(p)
if not acuan: # halaman tanpa lapisan teks -> tidak bisa dinilai
continue
for angka in sorted(_angka_dari_latex(x.get("text") or "")):
if angka not in acuan:
peringatan.append({
"jenis": "angka_tidak_ada_di_sumber", "item": i, "page": p,
"angka": angka,
"catatan": "angka pada formula tidak ditemukan di teks PDF asli",
})
# 4. Tabel tanpa isi
for i, x in enumerate(items):
if x.get("type") == "table" and not (x.get("table_body") or "").strip():
peringatan.append({"jenis": "tabel_kosong", "item": i, "page": x.get("page_idx")})
# 5. Chart tanpa konteks apa pun (tidak bisa dipakai tahap berikutnya)
for i, x in enumerate(items):
if x.get("type") == "chart":
punya = (x.get("content") or "").strip() or (x.get("chart_caption") or [])
if not punya:
peringatan.append({"jenis": "chart_tanpa_konteks", "item": i,
"page": x.get("page_idx")})
return {
"item": len(items),
"halaman_terdeteksi": len(halaman),
"per_tipe": dict(per_tipe),
"peringatan": peringatan,
"jumlah_peringatan": len(peringatan),
}