File size: 5,832 Bytes
5840d20 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """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),
}
|