Files
pfm-ocr/docs/simulasi_ekstraksi_lengkap.md
T

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 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).
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".
  • 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 <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).
  • 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

{
  "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, '');