feat: update backend OCR parser, web app, mobile app camera/preview UI, tests, and documentation with sample images
This commit is contained in:
1 parent
aa3233e411
commit
bdb3a49742
71 files changed
+3360
-764
No files matched your search
@@ -0,0 +1,264 @@
|
||||
# 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
|
||||
|
||||
<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:
|
||||
|
||||
```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, '');
|
||||
```
|
||||
Reference in new issue
Block a user