[NOTICKET] fix(knowledge-parsing): read word boundaries back from the source PDF
Browse filesMinerU writes formulas one character at a time ("T o t a l H o u r s"), and
once those are rejoined the word boundaries are gone: "Total Hours" and
"TotalHours" are no longer distinguishable from the LaTeX alone. Everything
fuses into a single token.
The multiplication written as a letter "x" is not a separate bug. It is the
same one — an "x" between terms is just another spaced character, so
"MOHH x Qty x PA x UA x Pty" collapses to "MOHHxQtyxPAxUAxPty".
Two obvious fixes were tried and rejected:
- Rewriting a lone "x" to a multiplication sign guesses, and guesses wrong
on any document with a variable named x, silently. It also leaves
"TotalHours" untouched, which is the larger half of the damage.
- Emitting a warning fires on 7 of 11 equations. A warning that lights up on
64% of items is not actionable, and recovers nothing.
The source PDF still holds the boundaries: its text layer reads
"MOHH x Qty x PA x UA x Pty", properly spaced. So the answer is read from the
document rather than guessed. A token that is not already a word on that page
is split only if EVERY piece is a word on that page, and only if exactly one
shortest such split exists. Ambiguity is left alone; so is anything that
cannot be segmented. It fails closed.
NFKC normalisation is required — formulas use a mathematical font, so the page
literally contains U+1D440 and friends, and without it nothing matches.
Correct documents are untouched by construction: a token already present as a
word never enters the segmenter, so a document writing \times produces tokens
that are all in the vocabulary and skips this path entirely. Verified — the
second test document changed in no way at all.
Threshold measured, not picked: 6, 4 and 3 are identical on equations, and 3
additionally recovers "MOHHx" and "xPA_A" in table cells. 2 starts doing
damage, splitting "PA_ij" into "PA_i j".
Result on the 9-page BUMA standard: glossary terms readable as whole tokens
320 -> 397, across 9 of 31 chunks. Chunk.latex is untouched, so the formula
branch still receives the original form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- src/knowledge_parsing/checks.py +34 -0
- src/knowledge_parsing/normalize.py +25 -7
- src/knowledge_parsing/render.py +95 -5
- src/knowledge_parsing/run.py +12 -2
|
@@ -12,6 +12,7 @@ turun diam-diam di antara ratusan dokumen.
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import re
|
|
|
|
| 15 |
from collections import Counter
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Any
|
|
@@ -64,6 +65,39 @@ def _angka_per_halaman(pdf: Path, min_digit: int = MIN_DIGIT) -> dict[int, set[s
|
|
| 64 |
return hasil
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def periksa_items(
|
| 68 |
items: list[dict[str, Any]],
|
| 69 |
min_latex_len: int = 8,
|
|
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import re
|
| 15 |
+
import unicodedata
|
| 16 |
from collections import Counter
|
| 17 |
from pathlib import Path
|
| 18 |
from typing import Any
|
|
|
|
| 65 |
return hasil
|
| 66 |
|
| 67 |
|
| 68 |
+
_KATA = re.compile(r"[A-Za-z][A-Za-z0-9]*")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def kosakata_per_halaman(pdf: Path) -> dict[int, frozenset[str]] | None:
|
| 72 |
+
"""Kata-kata pada lapisan teks PDF asli, per halaman.
|
| 73 |
+
|
| 74 |
+
Dipakai `render.pulihkan_batas_kata` sebagai wasit: MinerU menulis rumus per
|
| 75 |
+
karakter dan batas katanya hilang di situ, sementara PDF-nya masih menyimpan
|
| 76 |
+
batas itu utuh (`MOHH x Qty x PA`, bukan `MOHHxQtyxPAxUAxPty`).
|
| 77 |
+
|
| 78 |
+
NFKC dipakai karena rumus memakai font matematika — halaman ini menulis
|
| 79 |
+
`𝑀𝑂𝐻𝐻` (U+1D440…), bukan ASCII, dan tanpa normalisasi tidak ada satu kata
|
| 80 |
+
pun yang akan cocok.
|
| 81 |
+
|
| 82 |
+
Kembalikan None kalau PDF tidak terbaca; pemanggil lalu melewati pemulihan
|
| 83 |
+
diam-diam, sama seperti pemeriksaan angka di bawah.
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
from pypdf import PdfReader
|
| 87 |
+
reader = PdfReader(str(pdf))
|
| 88 |
+
except Exception:
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
hasil: dict[int, frozenset[str]] = {}
|
| 92 |
+
for i, hal in enumerate(reader.pages):
|
| 93 |
+
try:
|
| 94 |
+
teks = hal.extract_text() or ""
|
| 95 |
+
except Exception:
|
| 96 |
+
teks = ""
|
| 97 |
+
hasil[i] = frozenset(_KATA.findall(unicodedata.normalize("NFKC", teks)))
|
| 98 |
+
return hasil
|
| 99 |
+
|
| 100 |
+
|
| 101 |
def periksa_items(
|
| 102 |
items: list[dict[str, Any]],
|
| 103 |
min_latex_len: int = 8,
|
|
@@ -91,7 +91,7 @@ def _judul(item: dict[str, Any]) -> tuple[str | None, str | None, int | None]:
|
|
| 91 |
return m.group(1), m.group(2), m.group(1).count(".") + 1
|
| 92 |
|
| 93 |
|
| 94 |
-
def _teks_tabel(item: dict[str, Any]) -> str:
|
| 95 |
"""Caption + isi tabel sebagai teks terbaca.
|
| 96 |
|
| 97 |
HTML mentahnya TIDAK ditaruh di sini — disimpan terpisah di
|
|
@@ -99,7 +99,7 @@ def _teks_tabel(item: dict[str, Any]) -> str:
|
|
| 99 |
"""
|
| 100 |
bagian = list(item.get("table_caption") or [])
|
| 101 |
if item.get("table_body"):
|
| 102 |
-
bagian.append(render_table(item["table_body"]))
|
| 103 |
bagian += list(item.get("table_footnote") or [])
|
| 104 |
return "\n\n".join(b for b in bagian if b)
|
| 105 |
|
|
@@ -112,7 +112,21 @@ def _teks_chart(item: dict[str, Any]) -> str:
|
|
| 112 |
return "\n\n".join(b for b in bagian if b)
|
| 113 |
|
| 114 |
|
| 115 |
-
def normalisasi(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# Konteks bab per halaman, dari header berjalan
|
| 117 |
bab_per_halaman: dict[int, str] = {}
|
| 118 |
for x in items:
|
|
@@ -160,7 +174,7 @@ def normalisasi(items: list[dict[str, Any]], doc_id: str) -> list[Chunk]:
|
|
| 160 |
if tipe == "table":
|
| 161 |
tutup()
|
| 162 |
c = buka("table", page)
|
| 163 |
-
c.text = _teks_tabel(item)
|
| 164 |
c.is_tabular = True
|
| 165 |
c.table_html = item.get("table_body") or None
|
| 166 |
c.source_items = [i]
|
|
@@ -193,7 +207,7 @@ def normalisasi(items: list[dict[str, Any]], doc_id: str) -> list[Chunk]:
|
|
| 193 |
berjalan.images.append(item["img_path"])
|
| 194 |
if latex:
|
| 195 |
berjalan.latex.append(latex) # mentah, untuk cabang formula
|
| 196 |
-
terbaca = render_latex(latex)
|
| 197 |
if terbaca:
|
| 198 |
potongan.append(terbaca) # prosa, supaya NER menemukannya
|
| 199 |
continue
|
|
@@ -262,6 +276,10 @@ def normalisasi(items: list[dict[str, Any]], doc_id: str) -> list[Chunk]:
|
|
| 262 |
return chunks
|
| 263 |
|
| 264 |
|
| 265 |
-
def normalisasi_dari_berkas(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
items = json.loads(content_list.read_text(encoding="utf-8"))
|
| 267 |
-
return normalisasi(items, doc_id)
|
|
|
|
| 91 |
return m.group(1), m.group(2), m.group(1).count(".") + 1
|
| 92 |
|
| 93 |
|
| 94 |
+
def _teks_tabel(item: dict[str, Any], kosakata: frozenset[str] | None = None) -> str:
|
| 95 |
"""Caption + isi tabel sebagai teks terbaca.
|
| 96 |
|
| 97 |
HTML mentahnya TIDAK ditaruh di sini — disimpan terpisah di
|
|
|
|
| 99 |
"""
|
| 100 |
bagian = list(item.get("table_caption") or [])
|
| 101 |
if item.get("table_body"):
|
| 102 |
+
bagian.append(render_table(item["table_body"], kosakata))
|
| 103 |
bagian += list(item.get("table_footnote") or [])
|
| 104 |
return "\n\n".join(b for b in bagian if b)
|
| 105 |
|
|
|
|
| 112 |
return "\n\n".join(b for b in bagian if b)
|
| 113 |
|
| 114 |
|
| 115 |
+
def normalisasi(
|
| 116 |
+
items: list[dict[str, Any]],
|
| 117 |
+
doc_id: str,
|
| 118 |
+
kosakata_halaman: dict[int, frozenset[str]] | None = None,
|
| 119 |
+
) -> list[Chunk]:
|
| 120 |
+
"""`kosakata_halaman` opsional: kata-kata lapisan teks PDF asli per halaman.
|
| 121 |
+
|
| 122 |
+
Dipakai hanya untuk memulihkan batas kata yang hilang saat MinerU menulis
|
| 123 |
+
rumus per karakter (lihat `render.pulihkan_batas_kata`). Tanpa itu, hasilnya
|
| 124 |
+
persis seperti sebelumnya.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
def kosakata_dari(hal: int) -> frozenset[str] | None:
|
| 128 |
+
return (kosakata_halaman or {}).get(hal)
|
| 129 |
+
|
| 130 |
# Konteks bab per halaman, dari header berjalan
|
| 131 |
bab_per_halaman: dict[int, str] = {}
|
| 132 |
for x in items:
|
|
|
|
| 174 |
if tipe == "table":
|
| 175 |
tutup()
|
| 176 |
c = buka("table", page)
|
| 177 |
+
c.text = _teks_tabel(item, kosakata_dari(page))
|
| 178 |
c.is_tabular = True
|
| 179 |
c.table_html = item.get("table_body") or None
|
| 180 |
c.source_items = [i]
|
|
|
|
| 207 |
berjalan.images.append(item["img_path"])
|
| 208 |
if latex:
|
| 209 |
berjalan.latex.append(latex) # mentah, untuk cabang formula
|
| 210 |
+
terbaca = render_latex(latex, kosakata_dari(page))
|
| 211 |
if terbaca:
|
| 212 |
potongan.append(terbaca) # prosa, supaya NER menemukannya
|
| 213 |
continue
|
|
|
|
| 276 |
return chunks
|
| 277 |
|
| 278 |
|
| 279 |
+
def normalisasi_dari_berkas(
|
| 280 |
+
content_list: Path,
|
| 281 |
+
doc_id: str,
|
| 282 |
+
kosakata_halaman: dict[int, frozenset[str]] | None = None,
|
| 283 |
+
) -> list[Chunk]:
|
| 284 |
items = json.loads(content_list.read_text(encoding="utf-8"))
|
| 285 |
+
return normalisasi(items, doc_id, kosakata_halaman)
|
|
@@ -71,6 +71,86 @@ def _rapatkan(teks: str) -> str:
|
|
| 71 |
return _SPACED_RUN.sub(lambda m: m.group(0).replace(" ", ""), teks)
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def _argumen(s: str, i: int) -> tuple[str | None, int]:
|
| 75 |
"""Isi kurung SEIMBANG yang dimulai di `s[i] == '{'`.
|
| 76 |
|
|
@@ -122,8 +202,14 @@ def _render_frac(s: str) -> str:
|
|
| 122 |
return s
|
| 123 |
|
| 124 |
|
| 125 |
-
def render_latex(latex: str) -> str:
|
| 126 |
-
"""LaTeX -> a readable line. Best effort: it feeds a term filter, not a parser.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
s = latex.replace("$$", " ").replace("$", " ")
|
| 128 |
s = s.replace("~", _WORD_GAP)
|
| 129 |
|
|
@@ -169,7 +255,11 @@ def render_latex(latex: str) -> str:
|
|
| 169 |
s = _rapatkan(s)
|
| 170 |
|
| 171 |
s = s.replace(_WORD_GAP, " ")
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
|
| 175 |
# --- HTML tables ---------------------------------------------------------
|
|
@@ -184,7 +274,7 @@ _TAG = re.compile(r"<[^>]+>")
|
|
| 184 |
_INLINE_MATH = re.compile(r"\$([^$]{1,400})\$")
|
| 185 |
|
| 186 |
|
| 187 |
-
def render_table(table_html: str) -> str:
|
| 188 |
"""HTML table -> plain rows, cells separated by ' | '.
|
| 189 |
|
| 190 |
Keeps the reading order a person would use, so a term appearing only in a
|
|
@@ -194,7 +284,7 @@ def render_table(table_html: str) -> str:
|
|
| 194 |
s = _CELL_END.sub(" | ", s)
|
| 195 |
s = _TAG.sub(" ", s)
|
| 196 |
s = html.unescape(s)
|
| 197 |
-
s = _INLINE_MATH.sub(lambda m: " " + render_latex(m.group(1)) + " ", s)
|
| 198 |
|
| 199 |
baris = []
|
| 200 |
for raw in s.split("\n"):
|
|
|
|
| 71 |
return _SPACED_RUN.sub(lambda m: m.group(0).replace(" ", ""), teks)
|
| 72 |
|
| 73 |
|
| 74 |
+
# Panjang minimum token yang boleh dibelah. Diukur pada dokumen nyata: 6, 4 dan
|
| 75 |
+
# 3 memberi hasil identik pada rumus, dan 3 juga memulihkan `MOHHx` / `xPA_A` di
|
| 76 |
+
# sel tabel. Turun ke 2 mulai MERUSAK — `PA_ij` terbelah jadi `PA_i j`, karena
|
| 77 |
+
# `i` dan `j` dua-duanya kata yang sah di halaman itu.
|
| 78 |
+
MIN_TOKEN_SEGMEN = 3
|
| 79 |
+
_MAKS_KATA = 24 # panjang wajar satu kata; membatasi kerja DP
|
| 80 |
+
|
| 81 |
+
_TOKEN = re.compile(r"[A-Za-z][A-Za-z0-9]*")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _segmentasi(tok: str, kosakata: frozenset[str]) -> list[str] | None:
|
| 85 |
+
"""Belah `tok` menjadi kata-kata yang SEMUANYA ada di `kosakata`.
|
| 86 |
+
|
| 87 |
+
Kembalikan None kalau tidak ada pembelahan yang sah, ATAU kalau ada lebih
|
| 88 |
+
dari satu pembelahan terpendek yang sama sahnya. Ambiguitas dibiarkan apa
|
| 89 |
+
adanya — modul ini tidak menebak, dan sebuah token yang tidak bisa
|
| 90 |
+
dipulihkan dengan pasti lebih baik dibiarkan rusak daripada diperbaiki
|
| 91 |
+
ke bentuk yang salah tanpa jejak.
|
| 92 |
+
"""
|
| 93 |
+
n = len(tok)
|
| 94 |
+
# dp[i] = (jumlah kata minimum untuk tok[:i], berapa cara mencapainya, potongan)
|
| 95 |
+
dp: list[tuple[int, int, list[str]] | None] = [None] * (n + 1)
|
| 96 |
+
dp[0] = (0, 1, [])
|
| 97 |
+
|
| 98 |
+
for i in range(1, n + 1):
|
| 99 |
+
terbaik: int | None = None
|
| 100 |
+
cara = 0
|
| 101 |
+
potongan: list[str] = []
|
| 102 |
+
for j in range(max(0, i - _MAKS_KATA), i):
|
| 103 |
+
sebelum = dp[j]
|
| 104 |
+
if sebelum is None or tok[j:i] not in kosakata:
|
| 105 |
+
continue
|
| 106 |
+
panjang_j, cara_j, kata_j = sebelum
|
| 107 |
+
calon = panjang_j + 1
|
| 108 |
+
if terbaik is None or calon < terbaik:
|
| 109 |
+
terbaik, cara, potongan = calon, cara_j, kata_j + [tok[j:i]]
|
| 110 |
+
elif calon == terbaik:
|
| 111 |
+
cara += cara_j
|
| 112 |
+
if terbaik is not None:
|
| 113 |
+
dp[i] = (terbaik, cara, potongan)
|
| 114 |
+
|
| 115 |
+
hasil = dp[n]
|
| 116 |
+
if hasil is None:
|
| 117 |
+
return None
|
| 118 |
+
_, cara, potongan = hasil
|
| 119 |
+
if cara != 1 or len(potongan) < 2:
|
| 120 |
+
return None
|
| 121 |
+
return potongan
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def pulihkan_batas_kata(teks: str, kosakata: frozenset[str] | None) -> str:
|
| 125 |
+
"""Kembalikan spasi antar kata yang hilang saat deret berspasi dirapatkan.
|
| 126 |
+
|
| 127 |
+
MinerU menulis rumus per karakter (`T o t a l H o u r s`), dan begitu
|
| 128 |
+
dirapatkan batas katanya tidak bisa dipulihkan dari LaTeX-nya sendiri:
|
| 129 |
+
`Total Hours` dan `TotalHours` menjadi tidak terbedakan. Tanda kali yang
|
| 130 |
+
ditulis sebagai huruf `x` ikut melebur karena bentuknya sama saja dengan
|
| 131 |
+
karakter berspasi lain.
|
| 132 |
+
|
| 133 |
+
Lapisan teks PDF aslinya MASIH menyimpan batas itu, jadi jawabannya dibaca
|
| 134 |
+
dari dokumen alih-alih ditebak. Token yang sudah ada sebagai kata di sumber
|
| 135 |
+
tidak pernah disentuh — sehingga dokumen yang menulis rumusnya dengan benar
|
| 136 |
+
melewati jalur ini seluruhnya.
|
| 137 |
+
"""
|
| 138 |
+
if not kosakata:
|
| 139 |
+
return teks
|
| 140 |
+
|
| 141 |
+
ganti: dict[str, str] = {}
|
| 142 |
+
for tok in set(_TOKEN.findall(teks)):
|
| 143 |
+
if len(tok) < MIN_TOKEN_SEGMEN or tok in kosakata:
|
| 144 |
+
continue
|
| 145 |
+
potongan = _segmentasi(tok, kosakata)
|
| 146 |
+
if potongan:
|
| 147 |
+
ganti[tok] = " ".join(potongan)
|
| 148 |
+
|
| 149 |
+
for tok, jadi in sorted(ganti.items(), key=lambda kv: -len(kv[0])):
|
| 150 |
+
teks = re.sub(rf"(?<![A-Za-z0-9]){re.escape(tok)}(?![A-Za-z0-9])", jadi, teks)
|
| 151 |
+
return teks
|
| 152 |
+
|
| 153 |
+
|
| 154 |
def _argumen(s: str, i: int) -> tuple[str | None, int]:
|
| 155 |
"""Isi kurung SEIMBANG yang dimulai di `s[i] == '{'`.
|
| 156 |
|
|
|
|
| 202 |
return s
|
| 203 |
|
| 204 |
|
| 205 |
+
def render_latex(latex: str, kosakata: frozenset[str] | None = None) -> str:
|
| 206 |
+
"""LaTeX -> a readable line. Best effort: it feeds a term filter, not a parser.
|
| 207 |
+
|
| 208 |
+
`kosakata` is the source page's own word list, used only to put back word
|
| 209 |
+
boundaries this function cannot recover on its own — see
|
| 210 |
+
`pulihkan_batas_kata`. Omitting it reproduces the previous behaviour
|
| 211 |
+
exactly, so existing callers are unaffected.
|
| 212 |
+
"""
|
| 213 |
s = latex.replace("$$", " ").replace("$", " ")
|
| 214 |
s = s.replace("~", _WORD_GAP)
|
| 215 |
|
|
|
|
| 255 |
s = _rapatkan(s)
|
| 256 |
|
| 257 |
s = s.replace(_WORD_GAP, " ")
|
| 258 |
+
s = re.sub(r"\s+", " ", s).strip()
|
| 259 |
+
|
| 260 |
+
# Terakhir, setelah spasi normal kembali: pulihkan batas kata yang tidak
|
| 261 |
+
# bisa diketahui dari LaTeX-nya sendiri.
|
| 262 |
+
return pulihkan_batas_kata(s, kosakata)
|
| 263 |
|
| 264 |
|
| 265 |
# --- HTML tables ---------------------------------------------------------
|
|
|
|
| 274 |
_INLINE_MATH = re.compile(r"\$([^$]{1,400})\$")
|
| 275 |
|
| 276 |
|
| 277 |
+
def render_table(table_html: str, kosakata: frozenset[str] | None = None) -> str:
|
| 278 |
"""HTML table -> plain rows, cells separated by ' | '.
|
| 279 |
|
| 280 |
Keeps the reading order a person would use, so a term appearing only in a
|
|
|
|
| 284 |
s = _CELL_END.sub(" | ", s)
|
| 285 |
s = _TAG.sub(" ", s)
|
| 286 |
s = html.unescape(s)
|
| 287 |
+
s = _INLINE_MATH.sub(lambda m: " " + render_latex(m.group(1), kosakata) + " ", s)
|
| 288 |
|
| 289 |
baris = []
|
| 290 |
for raw in s.split("\n"):
|
|
@@ -20,7 +20,7 @@ import traceback
|
|
| 20 |
from datetime import datetime
|
| 21 |
from pathlib import Path
|
| 22 |
|
| 23 |
-
from .checks import periksa_items
|
| 24 |
from .config import PipelineConfig
|
| 25 |
from .contracts import ParsedDocument
|
| 26 |
from .manifest import CatatanDokumen, Manifest
|
|
@@ -76,8 +76,18 @@ def jalankan(cfg: PipelineConfig) -> Manifest:
|
|
| 76 |
pdf_sumber=source, offset_halaman=cfg.start_page,
|
| 77 |
)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
t0 = time.perf_counter()
|
| 80 |
-
chunks = normalisasi(items, doc_id)
|
| 81 |
catatan.detik_normalisasi = time.perf_counter() - t0
|
| 82 |
catatan.jumlah_chunk = len(chunks)
|
| 83 |
|
|
|
|
| 20 |
from datetime import datetime
|
| 21 |
from pathlib import Path
|
| 22 |
|
| 23 |
+
from .checks import kosakata_per_halaman, periksa_items
|
| 24 |
from .config import PipelineConfig
|
| 25 |
from .contracts import ParsedDocument
|
| 26 |
from .manifest import CatatanDokumen, Manifest
|
|
|
|
| 76 |
pdf_sumber=source, offset_halaman=cfg.start_page,
|
| 77 |
)
|
| 78 |
|
| 79 |
+
# Kosakata halaman sumber, untuk memulihkan batas kata yang hilang
|
| 80 |
+
# saat MinerU menulis rumus per karakter. Dikunci ulang ke indeks
|
| 81 |
+
# halaman MinerU: page_idx 0 berarti halaman `start_page` di PDF.
|
| 82 |
+
mentah = kosakata_per_halaman(source)
|
| 83 |
+
kosakata = (
|
| 84 |
+
{p - cfg.start_page: kata for p, kata in mentah.items()
|
| 85 |
+
if p >= cfg.start_page}
|
| 86 |
+
if mentah else None
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
t0 = time.perf_counter()
|
| 90 |
+
chunks = normalisasi(items, doc_id, kosakata)
|
| 91 |
catatan.detik_normalisasi = time.perf_counter() - t0
|
| 92 |
catatan.jumlah_chunk = len(chunks)
|
| 93 |
|