Teknis — OCR V3
Service PDF-to-Markdown OCR RAGA yang dibangun di atas Python + FastAPI. Berbeda dari generasi sebelumnya, service ini (ocr-proxy) tidak lagi menjalankan OCR engine sendiri — ia adalah lapisan orkestrasi yang memproksikan file PDF (upload atau URL) ke OCR processing engine eksternal (cloud maupun local), lalu mengambil alih penyimpanan hasil ke MinIO dan Elasticsearch, serta memicu Summarize secara otomatis setelah OCR selesai. Sistem menggunakan antrian in-memory dengan satu worker thread agar request besar tidak memblokir penerimaan request berikutnya. Digunakan oleh api-tarantula untuk memproses dokumen yang diunggah user.
Repository
| Key | Value |
|---|---|
| Git Remote | https://git.tlab.co.id/tarantula/tarantula-v2/service/ocr-proxy.git |
| Branch Aktif | main |
| Branch Deploy Staging | staging (CI/CD auto-build & deploy ke environment staging) |
git clone https://git.tlab.co.id/tarantula/tarantula-v2/service/ocr-proxy.git
cd ocr-proxyCatatan: repo ini bernama
ocr-proxydan bernaung di namespacetarantula-v2— berbeda dari generasi sebelumnya (ocr-v3di namespacetarantula-v3) yang menjalankan OCR engine (DotsOCRParser) secara langsung di dalam service. Pada arsitektur saat ini, pekerjaan OCR itu sendiri didelegasikan ke engine eksternal lewatCLOUD_PROCESSING_ENDPOINT/LOCAL_PROCESSING_ENDPOINT.
Tech Stack
| Layer | Teknologi |
|---|---|
| Runtime | Python 3.10 (Dockerfile) / 3.11 (Dockerfile.stag) |
| Framework | FastAPI + uvicorn |
| OCR Processing | Diproksi ke OCR engine eksternal (cloud/local) lewat HTTP — bukan engine internal |
| Object Storage | MinIO (minio SDK) |
| Search / Index | Elasticsearch (elasticsearch==8.15.0) |
| Integrasi Summarize | HTTP client (requests + retry adapter) ke service Summarize eksternal |
| Konversi File | LibreOffice / unoconv (office → PDF), Pillow (gambar → PDF), pypdf (gabung PDF) |
| Secret Management | infisical_sdk (Python) |
| Logging | loguru |
| Testing | pytest |
| Containerization | Docker (python:3.10/3.11-bookworm + poppler-utils, libreoffice, unoconv) |
Struktur Folder
ocr-proxy/
├── main.py # FastAPI app, antrian in-memory, worker thread, semua endpoint
├── converter_utils.py # FileConverter: konversi office/gambar ke PDF (LibreOffice/unoconv/fallback Python)
├── merge_utils.py # Download URL ke file sementara & gabungkan banyak PDF jadi satu (pypdf)
├── config/
│ └── infisical_config.py # Loader konfigurasi via Infisical (get_secret, fallback os.getenv)
├── services/
│ ├── elasticsearch_service.py # Index & query hasil OCR + summarize di Elasticsearch
│ ├── minio_service.py # Upload PDF hasil upload/merge ke MinIO
│ └── summarize_service.py # Panggil service Summarize eksternal + simpan hasilnya ke Elasticsearch
├── helper/
│ └── save_chunks_to_txt.py # Simpan hasil OCR (per-chunk) ke file txt/JSON sebagai input Summarize
├── tests/ # Unit test (pytest) — task status, Elasticsearch service, summarize service
├── .env.example
├── requirements.txt
├── Dockerfile # Image production
└── Dockerfile.stag # Image stagingEnvironment Variables
File .env / env container (bootstrap Infisical)
INFISICAL_PROJECT_ID=
INFISICAL_ENVIRONMENT=
INFISICAL_SECRET_PATH=
INFISICAL_CLIENT_ID=
INFISICAL_CLIENT_SECRET=
INFISICAL_HOST=| Variable | Default | Keterangan |
|---|---|---|
INFISICAL_PROJECT_ID | — | ID project Infisical (wajib) |
INFISICAL_ENVIRONMENT | staging | Environment target |
INFISICAL_SECRET_PATH | /ocrproxy | Path secrets di Infisical |
INFISICAL_CLIENT_ID | — | Client ID Universal Auth (wajib) |
INFISICAL_CLIENT_SECRET | — | Client Secret Universal Auth (wajib) |
INFISICAL_HOST | http://10.1.102.15:8002 | URL Infisical server |
Secrets via Infisical
OCR Processing Endpoint (Cloud / Local)
| Variable | Default | Keterangan |
|---|---|---|
CLOUD_PROCESSING_ENDPOINT | http://10.1.102.14:8010/process-pdf/ | Endpoint OCR engine eksternal untuk processing=cloud |
LOCAL_PROCESSING_ENDPOINT | http://localhost:9000/v1/process | Endpoint OCR engine eksternal untuk processing=local |
CLOUD_STATUS_PROCESSING_ENDPOINT | http://localhost:9000/v1/process | Endpoint status task pada engine cloud (harus diisi eksplisit di production) |
LOCAL_STATUS_PROCESSING_ENDPOINT | http://localhost:9000/v1/process | Endpoint status task pada engine local |
MAX_WAIT_OCR | 300 | Jumlah iterasi polling status upstream (2 detik/iterasi ⇒ ±10 menit sebelum 504 Task timeout) |
Elasticsearch
| Variable | Default | Keterangan |
|---|---|---|
ES_INDEX_NAME | pdf-parsing-results | Index penyimpanan chunk hasil OCR |
URL_ELASTIC | — | URL Elasticsearch server |
USER_ELASTIC | — | Username Elasticsearch |
PASS_ELASTIC | — | Password Elasticsearch |
VERIFY_ELASTIC | False | Verifikasi sertifikat TLS |
MinIO
| Variable | Default | Keterangan |
|---|---|---|
MINIO_ENDPOINT | — | Host MinIO |
MINIO_PORT | 9000 | Port MinIO |
MINIO_ACCESS_KEY | — | Access key MinIO |
MINIO_SECRET_KEY | — | Secret key MinIO |
MINIO_SSL | false | Gunakan SSL (true/false) |
MINIO_BUCKET | — | Nama bucket tujuan upload PDF |
MINIO_DOMAIN | — | Base URL publik yang dipakai untuk menyusun URL objek hasil upload |
Summarize
| Variable | Default | Keterangan |
|---|---|---|
SUMMARIZE_URL | http://10.1.102.14:8014 | URL service Summarize |
INDEX_DOCUMENT_SUMMARIZE | summarize_document | Index Elasticsearch untuk ringkasan penuh dokumen |
INDEX_DOCUMENT_SUMMARIZE_CHUNK | summarize_document_chunk | Index Elasticsearch untuk ringkasan per-chunk |
SUMMARIZE_CONNECT_TIMEOUT | 10 detik | Timeout koneksi ke Summarize |
SUMMARIZE_READ_TIMEOUT | 600 detik | Timeout baca response Summarize |
Endpoints
| Method | Path | Keterangan |
|---|---|---|
POST | /api/v1/pdfs | Upload file atau kirim URL PDF untuk di-OCR (async, entry point utama) |
GET | /api/v1/tasks/{task_id}/status | Cek status & progress task |
POST | /api/v1/pdfs/merge | Gabungkan beberapa file/URL jadi satu PDF, lalu OCR sebagai satu task |
GET | /api/v1/pdfs/{id} | Ambil hasil OCR tersimpan dari Elasticsearch (paginated, by pdf_id) |
POST | /api/v1/pdfs/bulk | Submit banyak URL sekaligus — masing-masing jadi task terpisah |
POST | /api/v1/pdfs/bulk/upload | Upload banyak file sekaligus — masing-masing jadi task terpisah |
POST | /api/v1/documents | Ingest dokumen bebas langsung ke index Elasticsearch |
GET | /api/v1/health | Health check (konektivitas Elasticsearch + status konfigurasi endpoint OCR) |
POST /api/v1/pdfs
Content-Type: multipart/form-data
file (UploadFile, opsional) — file PDF/gambar/office; salah satu dari file/url_file wajib diisi
url_file (str query, opsional) — URL file PDF
task_id (str query, opsional) — custom task ID
metadata_document (Form, opsional) — metadata JSON (string di multipart, atau body JSON langsung bila Content-Type: application/json)
type_ocr (str query, default "ekstrak_only") — "ekstrak_only" | "representasi"
representasi_count (int query, default 3) — jumlah gambar max untuk mode representasi
processing (str query, default "cloud") — "cloud" | "local", menentukan engine OCR tujuanCatatan: parameter
lang,parse_method,formula_enable, dantable_enabletidak lagi bisa diatur lewat request — nilainya dikunci di sisiocr-proxy(lang="en",parse_method="auto",formula_enable/table_enable=true) sebelum diteruskan ke OCR engine upstream.
Response (langsung, task masih pending):
{
"task_id": "task-1751234567",
"status": "pending",
"processing_type": "cloud"
}GET /api/v1/tasks/{task_id}/status
{
"task_id": "task-1751234567",
"status": "processing",
"stage": "ocr_polling",
"progress_pct": 45,
"stage_detail": "OCR engine processing document",
"created_at": "2026-06-30T10:00:00",
"started_at": "2026-06-30T10:00:02"
}status: pending → processing → done / failed. Kalau failed, response menyertakan error dan error_code.
Arsitektur Queue & Worker
Berbeda dari generasi sebelumnya (worker paralel + asyncio.Queue), ocr-proxy memakai satu worker thread tunggal yang membaca dari queue.Queue in-memory (tidak dibatasi ukuran) — task diproses serial, bukan paralel. Status tiap task disimpan di OrderedDict in-memory (task_status, maksimum 1.000 entri, entri terlama otomatis di-evict).
Task ditandai done segera setelah job summarize di-submit ke ThreadPoolExecutor — proses summarize berjalan asinkron di background dan tidak memblokir penyelesaian task OCR.
Tahapan Progress (stage / progress_pct)
progress_pct | stage | Keterangan |
|---|---|---|
5% | queued | Task diambil dari antrian |
10% | uploading | Mengirim file ke OCR engine |
20–80% | ocr_polling | Menunggu & polling status OCR engine upstream |
85% | downloading_result | Mengunduh hasil OCR dari upstream |
86% | uploading_pdf | Upload PDF ke MinIO (hanya jika task punya file lokal, bukan url_file murni) |
88% | indexing | Menyimpan hasil ke Elasticsearch |
95% | summarizing | Memicu proses ringkasan dokumen |
100% | done | Selesai |
Konversi & Penggabungan File
converter_utils.FileConverter— mengonversi file office (.docx,.doc,.pptx,.ppt,.xlsx,.xls,.csv) dan gambar (.jpg,.jpeg,.png,.bmp,.gif,.tiff,.webp) menjadi PDF. Untuk office: coba LibreOffice (soffice --headless) →unoconv→ fallback Python murni (khusus.docx, pakaipython-docx+reportlab).merge_utils.merge_pdfs_to_tempfile— menggabungkan beberapa PDF (lewatpypdf.PdfWriter) menjadi satu file, dipakai olehPOST /api/v1/pdfs/merge. File/URL non-PDF dikonversi dulu lewatFileConvertersebelum digabung.merge_utils.download_to_tempfile— mengunduh file dari URL ke file sementara (dipakai/pdfs/mergeuntuk inputurl_files).
Penyimpanan & Indexing
MinIO
Hanya dijalankan untuk task yang punya file lokal (hasil upload atau merge — bukan task url_file murni tanpa upload). PDF diunggah ke MINIO_BUCKET dengan nama objek ocr-documents/{task_id}.pdf, menghasilkan URL publik dari MINIO_DOMAIN yang kemudian disimpan sebagai output_pdf_file pada dokumen Elasticsearch.
Elasticsearch
ElasticsearchService.process_and_insert() memecah content_list dan md_content_list hasil OCR menjadi kelompok per 5 halaman (chunk_size=5), lalu mengindeks tiap chunk sebagai dokumen terpisah (id: {task_id}_chunk_{n}) ke index ES_INDEX_NAME (default pdf-parsing-results), termasuk metadata_document bila dikirim di request.
Struktur data tersimpan (contoh GET /api/v1/pdfs/{id}?page=1&size=1):
{
"page": 1,
"size": 1,
"total_documents": 3,
"total_pages": 3,
"documents": [
{
"id": "task-123_chunk_1",
"chunk_number": 1,
"pdf_name": "dokumen",
"pdf_id": "task-123",
"parse_method": "auto",
"uploaded_at": "2026-06-30T10:05:23",
"content_list": [{ "page": 1, "type": "text", "content": "..." }],
"md_content_list": [{ "page": 1, "markdown": "# Judul\n\n..." }],
"output_path": "2026/06",
"output_pdf_file": "https://minio-domain/bucket/ocr-documents/task-123.pdf",
"metadata_document": { "...": "..." }
}
]
}ElasticsearchService juga menyediakan ingest_data() / insert_data() / bulk_insert() sebagai helper generik — dipakai juga oleh SummarizeService untuk menulis hasil ringkasan.
Integrasi Summarize
Setelah hasil OCR di-index, OCRTextSaver menyimpan seluruh chunk ke file ./outputs/summarize_{task_id}.txt (format JSON). File ini dikirim (multipart, field file) ke {SUMMARIZE_URL}/summarize dengan chunk_size=10000, target_context=128000, enable_ner=true. Panggilan memakai retry otomatis (3x, backoff) untuk status 500/502/503/504.
Hasil dari Summarize disimpan ke Elasticsearch:
| Hasil | Index | Keterangan |
|---|---|---|
final_summary | INDEX_DOCUMENT_SUMMARIZE | Ringkasan penuh dokumen, id dokumen = task_id |
summarize_chunk (list) | INDEX_DOCUMENT_SUMMARIZE_CHUNK | Ringkasan per-chunk, id = {task_id}_{chunk_index}, di-bulk-insert |
Proses summarize berjalan di background thread (ThreadPoolExecutor, terpisah dari worker OCR) — kegagalan summarize dicatat ke log tapi tidak mengubah status task OCR yang sudah done. File txt input maupun file JSON output summarize dihapus otomatis setelah proses selesai.
Mode OCR (type_ocr)
| Mode | Keterangan |
|---|---|
ekstrak_only (default) | Ekstraksi teks saja, tanpa analisis gambar |
representasi | Teks + deskripsi gambar; jumlah gambar dibatasi representasi_count |
Nilai ini hanya diteruskan sebagai parameter ke OCR engine upstream (CLOUD_PROCESSING_ENDPOINT/LOCAL_PROCESSING_ENDPOINT) — logika ekstraksi & deskripsi gambar itu sendiri dijalankan oleh engine tersebut, bukan oleh ocr-proxy.
Health Check
GET /api/v1/health memeriksa cluster.health() Elasticsearch dan melaporkan apakah CLOUD_PROCESSING_ENDPOINT/LOCAL_PROCESSING_ENDPOINT terkonfigurasi. Mengembalikan 503 jika status Elasticsearch red/error, selain itu 200. MinIO, Summarize, dan OCR engine upstream tidak termasuk dalam pengecekan ini.
Build & Run
# Install dependencies
pip install -r requirements.txt
# Jalankan development
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# Docker build & run
docker build -t ocr-proxy .
docker run -p 8000:8000 \
-e INFISICAL_PROJECT_ID=... \
-e INFISICAL_ENVIRONMENT=staging \
-e INFISICAL_SECRET_PATH=/ocrproxy \
-e INFISICAL_CLIENT_ID=... \
-e INFISICAL_CLIENT_SECRET=... \
-e INFISICAL_HOST=http://10.1.102.15:8002 \
ocr-proxy