# 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](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/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 `PQ` untuk `PO`, huruf `O`/`I`/`S` tercampur 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*). ```markdown 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
Kode Barang Nama Barang Banyak Jumlah
11310014 Griller Size 2 (0.8-0.9) KG Frozen 2 40 PC
1131OO24 AYAM SIZE A PR FROZEN (O.9-1)KG/PC 1 20 PC
999999 Tanda Tangan Supir (Noise)
Barang dikirim dalam keadaan baik Jumlah 60 PC
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"`**. * **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"`**. --- ### 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): 1. Program mengambil potongan baris tepat di bawah baris label yang kosong. 2. Ditemukan kumpulan baris berisi nilai: * Baris 1: `23 Jun 2026` * Baris 2: `16O19B032l` * Baris 3: `165998O277` * Baris 4: `PQ/26/000023082B` 3. **Pemetaan Realinyasi**: * `tanggal` diarahkan mengambil baris yang memiliki pola tanggal $\rightarrow$ `"23 Jun 2026"`. * `noSO` diarahkan mengambil angka 10-digit pertama (termasuk karakter typo) $\rightarrow$ `"16O19B032l"`. * `noDO` diarahkan mengambil angka 10-digit kedua $\rightarrow$ `"165998O277"`. * `noPO` diarahkan 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 `O` diganti menjadi angka `0` (indeks 2). * Huruf `B` diganti menjadi angka `8` (indeks 5). * Huruf `l` (L kecil) diganti menjadi angka `1` (indeks 9). * *Hasil Bersih*: **`"1601980321"`** (Lolos validasi panjang 7-12 digit angka). #### 3. Koreksi & Sanitasi Nomor DO * *Teks Awal*: `"165998O277"` * *Proses*: Huruf `O` diganti menjadi angka `0` (indeks 6). * *Hasil Bersih*: **`"1659980277"`** (Lolos validasi panjang 7-12 digit angka). #### 4. Koreksi & Sanitasi Nomor PO * *Teks Awal*: `"PQ/26/000023082B"` * *Proses*: * Huruf `PQ` di depan diidentifikasi sebagai kesalahan baca dari label `PO`. Kode dibersihkan via `cleanAndFormatPO()`. * Huruf `B` di bagian belakang angka diganti menjadi angka `8`. * Tahun berjalan disesuaikan dengan segmentasi tahun dokumen (`26`). * *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: 1. **Tokenisasi**: Teks dipecah menjadi token-token kata: `['pfm', 'ancol', 'anc', '02', 'pademangan']` (kata seperti "jalan", "ke", "untuk" dibuang). 2. **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$). 3. **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 `` menjadi grid dua dimensi (baris & kolom), lalu menyaring baris: ```markdown 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). * **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"`**. #### Baris 2: * **OCR SKU**: `"1131OO24"` (Typo huruf `O`). * **Koreksi Visual Digit**: Huruf `O` diubah menjadi angka `0` $\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. * **Standardisasi Satuan**: * Kuantitas `"1"` diautocomplete dengan kemasan luar master (`jenis_outer: "Karung"`) $\rightarrow$ **`"1 KRG"`**. * Total `"20 PC"` distandardisasi menjadi **`"20 PC"`**. #### 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 ```json { "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` ```sql 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) ```sql 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, ''); ```