10 KiB
Dokumentasi Lengkap Alur Ekstraksi: Dari Teks Mentah (Raw Markdown) ke Data Bersih (JSON/Database)
Dokumen ini mendokumentasikan secara rinci bagaimana teks mentah (Raw Markdown) hasil pembacaan OCR & vLLM diolah langkah demi langkah oleh Next.js API Gateway (melalui utilitas parser.ts) hingga menjadi data terstruktur bersih (JSON) yang disimpan di database PostgreSQL.
1. Input: Teks Mentah (Raw Markdown) Hasil OCR & vLLM
Di bawah ini adalah contoh representatif dari teks Markdown mentah yang dihasilkan oleh pipeline FastAPI & vLLM (menggunakan model PaddleOCR-VL-1.6).
Teks ini memiliki beberapa masalah umum OCR:
- Visual Distortions / Typo: OCR salah mengenali karakter (misal
PQuntukPO, hurufO/I/Stercampur di dalam angka). - Shifted Layout: Teks label terpisah dari nilainya karena tata letak baris yang bergeser.
- Messy HTML Tables: Struktur tabel mengandung tag HTML, spasi bising, baris kosong, dan data tanda air (watermark).
PT. CHAROEN POKPHAND INDONESIA TBK
Kawasan Industri Modern Cikande, Serang, Banten
===================================================
Kepada Yth : PT.PRIMAFOOD INTERNATIONAL
Order Untuk : PFM ANCOL II (ANC-02)
Alamat Kirim : JL. ANCOL BARAT VIII NO. 1, PADEMANGAN, JAKARTA UTARA
Tanggal :
No. SO :
No. DO :
No. PO :
23 Jun 2026
16O19B032l
165998O277
PQ/26/000023082B
Plat Nomor : B 9427 UXT (ASLI)
Nama Driver : Ahmad Supriadi
<table border=1>
<tr>
<td>Kode Barang</td>
<td>Nama Barang</td>
<td>Banyak</td>
<td>Jumlah</td>
</tr>
<tr>
<td>11310014</td>
<td>Griller Size 2 (0.8-0.9) KG Frozen</td>
<td>2</td>
<td>40 PC</td>
</tr>
<tr>
<td>1131OO24</td>
<td>AYAM SIZE A PR FROZEN (O.9-1)KG/PC</td>
<td>1</td>
<td>20 PC</td>
</tr>
<tr>
<td>999999</td>
<td>Tanda Tangan Supir (Noise)</td>
<td></td>
<td></td>
</tr>
<tr>
<td colspan="2">Barang dikirim dalam keadaan baik</td>
<td>Jumlah</td>
<td>60 PC</td>
</tr>
</table>
Lembar 3 : Customer (ASLI)
2. Tahapan Pemrosesan & Ekstraksi Data (Step-by-Step)
Proses pemrosesan data mentah di atas dibagi menjadi 5 tahapan utama:
Tahap 1: Ekstraksi Regex Awal (Lapis 1)
Sistem pertama kali mencoba mengekstrak field utama menggunakan pola Regex.
- Ekstraksi Tanggal:
- Regex:
/Tanggal\s*[:\-.]?\s*([^\n]{6,100})/i - Hasil: Karena baris di samping label
"Tanggal :"kosong, pencarian langsung menghasilkan"Not Found".
- Regex:
- Ekstraksi No SO, DO, PO:
- Regex SO:
/(?:No\.?[ \t]*SO|SO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-]+)/i - Regex DO:
/(?:No\.?[ \t]*DO|Delivery Order[ \t]*No|D\.O\.[ \t]*No|Order[ \t]*No)[ \t]*[:\- \t]*([A-Z0-9\-]+)/i - Regex PO:
/(?:No\.?[ \t]*PO|PO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-\/]+)/i - Hasil: Sama seperti tanggal, label
"No. SO","No. DO", dan"No. PO"tidak memiliki nilai di baris yang sama, sehingga ketiganya menghasilkan"Not Found".
- Regex SO:
Tahap 2: Penyelarasan Layout Tergeser (Shifted Layout Re-alignment)
Karena semua field bernilai "Not Found", sistem mendeteksi adanya pergeseran layout. Program mengaktifkan algoritma pencarian baris terdekat (realinyasi indeks baris):
- Program mengambil potongan baris tepat di bawah baris label yang kosong.
- Ditemukan kumpulan baris berisi nilai:
- Baris 1:
23 Jun 2026 - Baris 2:
16O19B032l - Baris 3:
165998O277 - Baris 4:
PQ/26/000023082B
- Baris 1:
- Pemetaan Realinyasi:
tanggaldiarahkan mengambil baris yang memiliki pola tanggal\rightarrow"23 Jun 2026".noSOdiarahkan mengambil angka 10-digit pertama (termasuk karakter typo)\rightarrow"16O19B032l".noDOdiarahkan mengambil angka 10-digit kedua\rightarrow"165998O277".noPOdiarahkan mengambil baris berpola PO\rightarrow"PQ/26/000023082B".
Tahap 3: Sanitasi Data & Koreksi Karakter Typo (Lapis 2)
Setelah field berhasil dipetakan, program menjalankan fungsi sanitizeParsedMetadata() untuk membersihkan kesalahan visual OCR:
1. Koreksi & Sanitasi Tanggal
- Teks Awal:
"23 Jun 2026" - Proses: Regex membagi string menjadi Hari (
23), Bulan (Jun), dan Tahun (2026). Bulan"Jun"dicocokkan ke map kamus bulan untuk dikembangkan. - Hasil Bersih:
"23 June 2026"
2. Koreksi & Sanitasi Nomor SO
- Teks Awal:
"16O19B032l" - Proses: Fungsi
correctVisualDigits()memindai karakter non-angka dan menggantinya berdasarkan tabel kemiripan bentuk:- Huruf
Odiganti menjadi angka0(indeks 2). - Huruf
Bdiganti menjadi angka8(indeks 5). - Huruf
l(L kecil) diganti menjadi angka1(indeks 9).
- Huruf
- Hasil Bersih:
"1601980321"(Lolos validasi panjang 7-12 digit angka).
3. Koreksi & Sanitasi Nomor DO
- Teks Awal:
"165998O277" - Proses: Huruf
Odiganti menjadi angka0(indeks 6). - Hasil Bersih:
"1659980277"(Lolos validasi panjang 7-12 digit angka).
4. Koreksi & Sanitasi Nomor PO
- Teks Awal:
"PQ/26/000023082B" - Proses:
- Huruf
PQdi depan diidentifikasi sebagai kesalahan baca dari labelPO. Kode dibersihkan viacleanAndFormatPO(). - Huruf
Bdi bagian belakang angka diganti menjadi angka8. - Tahun berjalan disesuaikan dengan segmentasi tahun dokumen (
26).
- Huruf
- Hasil Bersih:
"PO/26/0000230828"
5. Koreksi Plat Nomor
- Teks Awal:
"B 9427 UXT (ASLI)" - Proses: Kata sampingan
(ASLI)dibuang. Pola plat nomor Indonesia (B 9427 UXT) dicocokkan dengan kode prefix wilayah terdaftar. - Hasil Bersih:
"B 9427 UXT"
Tahap 4: Fuzzy Store Resolution (Pencocokan Toko)
Teks customer "PT.PRIMAFOOD INTERNATIONAL Order Untuk: PFM ANCOL II (ANC-02) Alamat Kirim: JL. ANCOL BARAT VIII..." dianalisis:
- Tokenisasi: Teks dipecah menjadi token-token kata:
['pfm', 'ancol', 'anc', '02', 'pademangan'](kata seperti "jalan", "ke", "untuk" dibuang). - Kalkulasi Irisan Token:
Sistem mengompilasi token store dari database
store_master.- Entri Toko:
nama_toko: "PX HEAD OFFICE ANCOL",alamat: "JL. ANCOL BARAT VIII NO. 1" - Irisan: Token
'ancol','barat','viii', dan'pademangan'memiliki kecocokan tinggi (skor>0.8).
- Entri Toko:
- Hasil Resolusi:
orderUntuk\rightarrow"PX HEAD OFFICE ANCOL"(Nama resmi dari DB).alamat\rightarrow"JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA"(Alamat resmi dari DB).
Tahap 5: Parsing Tabel & Validasi Triple-Check Barang
Sistem mengurai tag <table> menjadi grid dua dimensi (baris & kolom), lalu menyaring baris:
Baris 1: | 11310014 | Griller Size 2 (0.8-0.9) KG Frozen | 2 | 40 PC |
Baris 2: | 1131OO24 | AYAM SIZE A PR FROZEN (O.9-1)KG/PC | 1 | 20 PC |
Baris 3: | 999999 | Tanda Tangan Supir (Noise) | | |
Baris 1:
- OCR SKU:
"11310014"(Valid 8-digit). - Fuzzy Database Matching (Triple-Check):
- Dilakukan pencarian di database
sku_master. Kode SKU"11310014"terdaftar sebagai'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)'. - Karena SKU cocok 100%, sistem mengunci data master ini (Skor = 1.0).
- Dilakukan pencarian di database
- Standardisasi Satuan:
- Kuantitas
"2"diautocomplete dengan kemasan luar master (jenis_outer: "Karung")\rightarrow"2 KRG". - Total
"40 PC"dikoreksi dengan kemasan dalam master (standar_jumlah: "PC")\rightarrow"40 PC".
- Kuantitas
Baris 2:
- OCR SKU:
"1131OO24"(Typo hurufO). - Koreksi Visual Digit: Huruf
Odiubah menjadi angka0\rightarrow"11310024". - Fuzzy Database Matching (Triple-Check):
- SKU hasil koreksi
"11310024"dicari di DB, terdaftar sebagai'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)'. - SKU cocok 100%. Data master dikunci.
- SKU hasil koreksi
- Standardisasi Satuan:
- Kuantitas
"1"diautocomplete dengan kemasan luar master (jenis_outer: "Karung")\rightarrow"1 KRG". - Total
"20 PC"distandardisasi menjadi"20 PC".
- Kuantitas
Baris 3:
- OCR SKU:
"999999". - Validasi SKU: Gagal karena panjang hanya 6 digit.
- Hasil: Baris dibuang sebagai noise.
Baris 4 (Baris Total):
- Terbaca
"Jumlah"dan"60 PC". - Dideteksi sebagai watermark/kolom ringkasan melalui fungsi
isWatermark(). - Hasil: Baris dibuang.
3. Output: JSON Hasil Akhir & Struktur Database
Setelah melalui seluruh pipeline pemrosesan di atas, data yang dikembalikan ke aplikasi mobile Flutter dan disimpan ke database PostgreSQL berbentuk data bersih berikut:
Objek JSON Metadata Akhir
{
"id": "42",
"filePath": "1782870899198-sample_do.jpeg",
"createdAt": "2026-07-02T03:56:00.000Z",
"header": {
"tanggal": "23 June 2026",
"no_po": "PO/26/0000230828",
"no_so": "1601980321",
"no_do": "1659980277"
},
"shipment": {
"kepada_yth": "PT.PRIMAFOOD INTERNATIONAL",
"order_untuk": "PX HEAD OFFICE ANCOL",
"alamat": "JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA",
"plat_truk": "B 9427 UXT",
"nama_driver": "Ahmad Supriadi",
"nama_penerima": ""
},
"items": [
{
"nomor_sku": "11310014",
"nama_barang": "AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)",
"banyak": "2 KRG",
"jumlah": "40 PC"
},
{
"nomor_sku": "11310024",
"nama_barang": "AYAM SIZE A FROZEN (0.9-1)KG/PC(*)",
"banyak": "1 KRG",
"jumlah": "20 PC"
}
],
"latitude": -6.1284,
"longitude": 106.8427
}
Penyimpanan ke Tabel PostgreSQL
1. Baris Baru di Tabel documents
INSERT INTO documents (id, filename, upload_time, size, parsed, metadata, is_sample, file_hash, latitude, longitude)
VALUES (
42,
'1782870899198-sample_do.jpeg',
'2026-07-02 03:56:00',
151816,
true,
'{"header": {"tanggal": "23 June 2026", "no_po": "PO/26/0000230828", ...}, "shipment": {...}}',
false,
'd4a183...561fe99',
-6.1284,
106.8427
);
2. Baris Baru di Tabel ocr_items (Bulk Insert)
INSERT INTO ocr_items (document_id, row_index, kode_barang_original, kode_barang, nama_barang, banyak_original, banyak, jumlah_original, jumlah, is_flagged, remark)
VALUES
(42, 0, '11310014', '11310014', 'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)', '2', '2 KRG', '40 PC', '40 PC', false, ''),
(42, 1, '1131OO24', '11310024', 'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)', '1', '1 KRG', '20 PC', '20 PC', false, '');