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),
    }