| """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 |
|
|
| |
| _ADA_ANGKA = re.compile(r"\d") |
|
|
| |
| |
| |
| |
| |
| |
| 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 = "" |
| |
| 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]] = [] |
|
|
| |
| 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}) |
|
|
| |
| 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]}) |
|
|
| |
| |
| |
| |
| 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: |
| 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", |
| }) |
|
|
| |
| 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")}) |
|
|
| |
| 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), |
| } |
|
|