v1.0.0-011 : [DONE] Optimasi OpenCV Dynamic Bounding Box
Rencana penerapan Contour Detection OpenCV secara dinamis untuk mendeteksi area nota demi memangkas latensi.
Tanggal Update: 25 Juli 2026
Setelah melakukan upload nota yang sama berkali-kali untuk pengujian OCR, kami menyadari bahwa meskipun OpenCV sudah aktif di dalam pipeline (sebagai vision_matcher.py), OpenCV tidak pernah berhasil memotong (crop) gambar untuk mengecilkan ukuran data. Hal ini menyebabkan sistem selalu melakukan fallback dengan mengirim gambar utuh ke Gemini AI, yang mengakibatkan tingginya latensi pemrosesan (5-15 detik).
Proses template matching gagal memotong gambar karena pada saat nota baru dipelajari oleh AI, koordinat potong (bounding box) disimpan ke dalam database MCP dengan nilai kaku (hardcoded):
{"x": 0, "y": 0, "w": 0, "h": 0}
Karena width (w) dan height (h) bernilai 0, saat OpenCV mencoba memotong gambar pada upload berikutnya, sistem keamanan OpenCV membatalkannya karena menganggap koordinat 0x0 tidak valid.
Kami mengubah pendekatan hardcode menjadi dinamis dengan memanfaatkan fitur Contour Detection dari OpenCV.
- Saat nota diproses dan dipelajari (Fase 4), sistem akan melakukan deteksi otomatis area yang mengandung tulisan/tabel (edges/contours).
- Sistem secara otomatis menghitung koordinat titik tepi terluar teks (dynamic bounding box) dengan tambahan sedikit ruang kosong (padding).
- Koordinat dinamis inilah yang akan dihafal oleh MCP Server. Sehingga pada upload berikutnya, OpenCV dapat secara presisi memotong gambar hanya pada area kontennya saja, membuang latar belakang kosong atau kotoran di pinggir gambar, sehingga pengiriman data ke LLM menjadi jauh lebih ringan dan cepat.
Berdasarkan uji coba langsung (stopwatch: 1 menit 30 detik) setelah deploy pembaruan, ditemukan beberapa kendala yang masih perlu diatasi. Berikut adalah analisisnya:
Meskipun fitur Dynamic Bounding Box dan perbaikan tool di MCP sudah dilakukan, delay panjang ini terjadi bukan karena LLM yang lambat, melainkan karena kegagalan jaringan internal (Crash-Loop) pada saat transisi.
- Kronologi Log: Saat proses
pnpm builduntukmcp-serverberjalan di background, proses tersebut sempat gagal karena error type-checking (TypeScript). Namun, instruksi deploy berlanjut dan memicu wadah (container)mcp-backend-produntuk terus melakukan restart loop. - Dampak: Saat aplikasi utama (Sajen) mencoba mengirim gambar ke MCP untuk proses OpenCV (
match_visual_template), koneksi gagal dengan error[Errno -3] Temporary failure in name resolutionkarena container MCP sedang mati. - Efek Domino: Kegagalan ini memaksa Sajen menunggu respons yang berujung pada timeout (pengalihan ke Ollama lokal yang juga timeout), sebelum akhirnya sistem beralih penuh ke Gemini. Rantai kegagalan koneksi inilah yang menyebabkan total waktu tunggu mencapai 90 detik.
Ketidakakuratan ekstraksi kuantitas ini umumnya disebabkan oleh cara Gemini (atau fallback LLM) menerjemahkan angka desimal pada gambar beresolusi rendah atau yang dipotong dengan komposisi yang salah:
- Konteks Teks: Pada beberapa nota cetak, jarak antara angka
2dan.00sangat berdekatan atau buram. Model bahasa besar (LLM) seperti Gemini cenderung menginterpretasikan angka di kolom Qty sebagai integer utuh (bilangan bulat). - Prompting: Jika di dalam system prompt LLM OCR tidak dijelaskan secara eksplisit untuk membaca bilangan desimal sebagai desimal, LLM akan “memperbaiki”
2.00menjadi 1 atau bilangan default karena menganggapnya sebagai typo.
Ini sangat berkaitan erat dengan sistem Dynamic Bounding Box atau efek ketika MCP sedang gagal diakses:
- Kegagalan Template Matching: Karena tadi MCP sedang tidak bisa diakses, sistem menggunakan LLM secara penuh. Terkadang LLM kesulitan menemukan hirarki (nama toko, alamat, list barang) jika gambar dikirim secara utuh tanpa panduan area potong.
- Struktur Nota: Nama supplier biasanya terletak di bagian paling atas nota (header). Jika kontras header terlalu rendah, atau jika letaknya terpisah jauh dari daftar barang (items), ada risiko instruksi prompt LLM hanya berfokus pada keranjang barang dan membiarkan field nama supplier kosong (Unknown).
Tindakan Selanjutnya: Kami akan memperbaiki instruksi prompt pada fase OCR untuk mengekstraksi format desimal secara persis, serta memastikan stabilitas layanan MCP Server agar OpenCV dan proses LLM berjalan di bawah 10 detik.
Pada uji coba lanjutan, terdapat dua keluhan baru dari pengguna:
- Nota pembelian dari supplier (seperti LUWES WINONG) disalahartikan sebagai “Transaksi Penjualan” yang berakibat pada salahnya pencatatan jurnal akuntansi.
- Penggunaan token membludak, terbukti dari log yang menunjukkan input sebanyak 431 token dan output 736 token untuk satu struk sederhana.
Setelah dilakukan analisis langsung pada gambar asli (WhatsApp Image 2026-07-25 at 20.55.03.jpeg), tidak terdapat tulisan “Nota Penjualan” sama sekali pada kertas struk. AI berhalusinasi dan mengasumsikan tipe transaksi sebagai sales (penjualan) semata-mata karena mengenali format tersebut sebagai format struk kasir ritel (dari sudut pandang toko Luwes, itu memang penjualan mereka). Oleh karena itu, kami mengubah strategi prompting OCR:
- Solusi: Kami menambahkan instruksi tegas pada prompt di
mcp-serverdansajenagar AI tidak terpaku pada format ritel, melainkan memposisikan dirinya sebagai pembeli: “Jika nota diterbitkan oleh pihak eksternal (minimarket, grosir, supplier) kepada kita, maka transaction_type WAJIB diset ‘purchase’”. - Di sisi frontend (
blonjo-ui), kami juga memastikan bahwa setiap kali OCR berhasil mendeteksi nama contact_name (supplier), UI langsung mem-prefix teks dengan kata “Pembelian di…” untuk memaksa backend mendeteksinya sebagai transaksi purchase.
Lonjakan token input (431 token) disebabkan oleh masuknya data Pricing Rules (Aturan Harga Jual bertingkat) dari RAG Context.
- Kronologi: Ketika teks hasil OCR (“Transaksi Penjualan di LUWES WINONG…”) dikirim ke Smart Parser, kata “Penjualan” langsung direspons oleh parser dengan menginjeksi seluruh konfigurasi harga jual toko dari database ke dalam prompt AI.
- Solusi: Aturan harga jual (seperti diskon member, harga grosir) hanya relevan jika kita sedang berjualan. Untuk transaksi pengeluaran/pembelian (purchase), informasi katalog dan pricing rules sama sekali tidak berguna dan hanya membuang-buang token. Kami memodifikasi logika Smart Parser di
sajen(ai_context.pydansmart_parser.py) agar otomatis mengabaikan dan membuang seluruh data pricing rules dari RAG context bila transaksi terdeteksi sebagai pembelian.
Kedua langkah di atas tidak hanya memperbaiki cacat logika pada jurnal akuntansi, tetapi juga akan langsung memangkas tagihan kuota token hingga 60-80% per struk pembelian.
Pada uji coba terakhir, sistem berhasil mengklasifikasikan transaksi sebagai “purchase”, akan tetapi hasil rincian (items) sedikit meleset. Terdapat penggabungan otomatis (agregasi) untuk barang-barang dengan nama yang sama persis (misal: SOKLIN SOFTERG), di mana sistem menjumlahkan qty secara otomatis, tetapi sayangnya sistem hanya mengambil harga satuan (unit_price) dari data yang pertama muncul. Padahal di dunia nyata, barang dengan nama yang sama bisa memiliki varian rasa/ukuran atau harga promo yang berbeda.
- Penyebab: LLM (Gemini) secara naluriah (bawaan) mencoba untuk “merapikan” hasil dan menyederhanakan output dengan melakukan agregasi pada item yang dianggap repetitif/ganda.
- Solusi: Kami menyuntikkan larangan keras (Rule baru) pada instruksi prompting di sisi
mcp-server(transactionParser.ts): DILARANG MENGGABUNGKAN BARANG (NO AGGREGATION). AI diinstruksikan untuk menulis semua item persis baris per baris (apa adanya), dan dilarang menjumlahkan kuantitas atau menggabungkan item dengan nama yang sama, sehingga setiap baris harga pada struk terekam sebagai objek terpisah dengan nominal total dan unit_price yang akurat.
Pada uji coba terakhir, pengguna mengeluhkan total waktu proses mencapai 4 menit 31 detik, meskipun hasil OCR (Gemini Vision) sudah sempurna. Keterlambatan ekstrem ini terjadi bukan pada fase pembacaan gambar (OCR), melainkan pada fase Parsing Transaksi (Text Generation) di dalam mcp-server.
- Kronologi di dalam AI Provider (
mcp-server/src/services/aiProviderService.ts):- Untuk pemrosesan gambar (Vision), urutannya adalah Gemini -> Ollama. Sehingga OCR berjalan sangat cepat.
- Namun, untuk pemrosesan teks (
generateTextyang dipakai olehparse_transaction), sistem memprioritaskan Ollama (Local & Ngrok) di atas Gemini. - Karena aplikasi berjalan di VPS, Ollama Local langsung gagal (di-skip).
- Sistem kemudian beralih ke Ollama Ngrok. Pengecekan status (health check) Ngrok berhasil, sehingga sistem mengira Ollama siap digunakan.
- Sistem mulai melakukan looping untuk mencoba model-model di
OLLAMA_GEN_MODELS(contoh: qwen2.5-coder, llama3, dll). - Masalahnya, timeout untuk setiap model Ollama Ngrok di-hardcode selama 60 detik (1 menit). Jika komputer lokal/ngrok pengguna sedang sibuk, kehabisan VRAM, atau tertidur (sleep), permintaan ini akan terus menggantung (hanging) hingga batas waktu habis.
- Jika ada 4 model dalam daftar, sistem akan menghabiskan 4 x 60 detik = 4 menit hanya untuk menunggu timeout Ollama, sebelum akhirnya menyerah dan menggunakan fallback ke Gemini API yang sebenarnya bisa menyelesaikan tugas tersebut dalam hitungan detik.
- Kesimpulan: Keterlambatan 4,5 menit murni disebabkan oleh Fallback Chain yang mencoba memaksa penggunaan Ollama via Ngrok dengan batas timeout yang sangat panjang per model.
Merespons temuan di poin 4, kami mengambil langkah tegas untuk menyamakan prioritas Text Generation agar sama dengan alur Vision:
- Kode di dalam
mcp-server/src/services/aiProviderService.tsdirombak. - Gemini (Free Tier) kini dinaikkan menjadi STEP 1 (Prioritas Utama) untuk parsing teks. Ini akan menjamin proses OCR dan parsing JSON berjalan super cepat tanpa tersendat.
- Mekanisme pergantian (switch) API Key secara otomatis pada rantai (chain) Gemini tetap terjaga, sehingga jika salah satu kuota habis, ia akan otomatis lompat ke key cadangan.
- Ollama Local dan Ollama Ngrok diturunkan derajatnya menjadi Fallback 1 dan Fallback 2. Jika Gemini API down secara global, sistem baru akan rela menunggu dan beralih ke Ollama. Dengan perbaikan ini, latensi 4,5 menit yang menjengkelkan dipastikan tidak akan terjadi lagi di kondisi normal.
Berdasarkan temuan inkonsistensi respons paska perbaikan performa parsing, dijadwalkan beberapa penambalan arsitektural berikut (belum dieksekusi):
- [DONE] Eliminasi Leaky Fallback (OCR Vision): Menghapus jalur fallback ke Ollama LLaVA di fungsi
generateVision. OCR murni akan mengandalkan rotasi API Key Gemini untuk mencegah timeout 2,5 menit dan halusinasi fatal. FileocrTool.tspadamcp-serverjuga telah disesuaikan agar tidak memulai pemrosesan gambar dengan Ollama. - [DONE] Verifikasi Matematis Harga (Parsing): Menambah aturan prompt tegas agar LLM membedakan secara logis antara “Harga Total” dan “Harga Satuan”, meminimalisir kesalahan perhitungan saat simbol
@absen di struk supplier. - [DONE] Penanganan Tanggal Literal (Parsing): Merombak format placeholder tanggal pada prompt (dari
YYYY-MM-DDke format variabel) agar LLM tidak menyalin teks literal saat tanggal di struk kabur. - [DONE] State-Clearance Anti Garbage: Menginvestigasi dan memastikan fungsi
resetState()berjalan bersih. Pada frontend (blonjo-ui), input file telah di-reset secara eksplisit (fileInput.value = '') pada saathandleFullResetdipanggil, sehingga tidak ada state atau file upload yang tersisa atau menggantung pada transaksi berikutnya.