Skip to content

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

KeyValue
Git Remotehttps://git.tlab.co.id/tarantula/tarantula-v2/service/ocr-proxy.git
Branch Aktifmain
Branch Deploy Stagingstaging (CI/CD auto-build & deploy ke environment staging)
bash
git clone https://git.tlab.co.id/tarantula/tarantula-v2/service/ocr-proxy.git
cd ocr-proxy

Catatan: repo ini bernama ocr-proxy dan bernaung di namespace tarantula-v2 — berbeda dari generasi sebelumnya (ocr-v3 di namespace tarantula-v3) yang menjalankan OCR engine (DotsOCRParser) secara langsung di dalam service. Pada arsitektur saat ini, pekerjaan OCR itu sendiri didelegasikan ke engine eksternal lewat CLOUD_PROCESSING_ENDPOINT/LOCAL_PROCESSING_ENDPOINT.

Tech Stack

LayerTeknologi
RuntimePython 3.10 (Dockerfile) / 3.11 (Dockerfile.stag)
FrameworkFastAPI + uvicorn
OCR ProcessingDiproksi ke OCR engine eksternal (cloud/local) lewat HTTP — bukan engine internal
Object StorageMinIO (minio SDK)
Search / IndexElasticsearch (elasticsearch==8.15.0)
Integrasi SummarizeHTTP client (requests + retry adapter) ke service Summarize eksternal
Konversi FileLibreOffice / unoconv (office → PDF), Pillow (gambar → PDF), pypdf (gabung PDF)
Secret Managementinfisical_sdk (Python)
Loggingloguru
Testingpytest
ContainerizationDocker (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 staging

Environment Variables

File .env / env container (bootstrap Infisical)

bash
INFISICAL_PROJECT_ID=
INFISICAL_ENVIRONMENT=
INFISICAL_SECRET_PATH=
INFISICAL_CLIENT_ID=
INFISICAL_CLIENT_SECRET=
INFISICAL_HOST=
VariableDefaultKeterangan
INFISICAL_PROJECT_IDID project Infisical (wajib)
INFISICAL_ENVIRONMENTstagingEnvironment target
INFISICAL_SECRET_PATH/ocrproxyPath secrets di Infisical
INFISICAL_CLIENT_IDClient ID Universal Auth (wajib)
INFISICAL_CLIENT_SECRETClient Secret Universal Auth (wajib)
INFISICAL_HOSThttp://10.1.102.15:8002URL Infisical server

Secrets via Infisical

OCR Processing Endpoint (Cloud / Local)

VariableDefaultKeterangan
CLOUD_PROCESSING_ENDPOINThttp://10.1.102.14:8010/process-pdf/Endpoint OCR engine eksternal untuk processing=cloud
LOCAL_PROCESSING_ENDPOINThttp://localhost:9000/v1/processEndpoint OCR engine eksternal untuk processing=local
CLOUD_STATUS_PROCESSING_ENDPOINThttp://localhost:9000/v1/processEndpoint status task pada engine cloud (harus diisi eksplisit di production)
LOCAL_STATUS_PROCESSING_ENDPOINThttp://localhost:9000/v1/processEndpoint status task pada engine local
MAX_WAIT_OCR300Jumlah iterasi polling status upstream (2 detik/iterasi ⇒ ±10 menit sebelum 504 Task timeout)

Elasticsearch

VariableDefaultKeterangan
ES_INDEX_NAMEpdf-parsing-resultsIndex penyimpanan chunk hasil OCR
URL_ELASTICURL Elasticsearch server
USER_ELASTICUsername Elasticsearch
PASS_ELASTICPassword Elasticsearch
VERIFY_ELASTICFalseVerifikasi sertifikat TLS

MinIO

VariableDefaultKeterangan
MINIO_ENDPOINTHost MinIO
MINIO_PORT9000Port MinIO
MINIO_ACCESS_KEYAccess key MinIO
MINIO_SECRET_KEYSecret key MinIO
MINIO_SSLfalseGunakan SSL (true/false)
MINIO_BUCKETNama bucket tujuan upload PDF
MINIO_DOMAINBase URL publik yang dipakai untuk menyusun URL objek hasil upload

Summarize

VariableDefaultKeterangan
SUMMARIZE_URLhttp://10.1.102.14:8014URL service Summarize
INDEX_DOCUMENT_SUMMARIZEsummarize_documentIndex Elasticsearch untuk ringkasan penuh dokumen
INDEX_DOCUMENT_SUMMARIZE_CHUNKsummarize_document_chunkIndex Elasticsearch untuk ringkasan per-chunk
SUMMARIZE_CONNECT_TIMEOUT10 detikTimeout koneksi ke Summarize
SUMMARIZE_READ_TIMEOUT600 detikTimeout baca response Summarize

Endpoints

MethodPathKeterangan
POST/api/v1/pdfsUpload file atau kirim URL PDF untuk di-OCR (async, entry point utama)
GET/api/v1/tasks/{task_id}/statusCek status & progress task
POST/api/v1/pdfs/mergeGabungkan 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/bulkSubmit banyak URL sekaligus — masing-masing jadi task terpisah
POST/api/v1/pdfs/bulk/uploadUpload banyak file sekaligus — masing-masing jadi task terpisah
POST/api/v1/documentsIngest dokumen bebas langsung ke index Elasticsearch
GET/api/v1/healthHealth 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 tujuan

Catatan: parameter lang, parse_method, formula_enable, dan table_enable tidak lagi bisa diatur lewat request — nilainya dikunci di sisi ocr-proxy (lang="en", parse_method="auto", formula_enable/table_enable=true) sebelum diteruskan ke OCR engine upstream.

Response (langsung, task masih pending):

json
{
  "task_id": "task-1751234567",
  "status": "pending",
  "processing_type": "cloud"
}

GET /api/v1/tasks/{task_id}/status

json
{
  "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: pendingprocessingdone / 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_pctstageKeterangan
5%queuedTask diambil dari antrian
10%uploadingMengirim file ke OCR engine
20–80%ocr_pollingMenunggu & polling status OCR engine upstream
85%downloading_resultMengunduh hasil OCR dari upstream
86%uploading_pdfUpload PDF ke MinIO (hanya jika task punya file lokal, bukan url_file murni)
88%indexingMenyimpan hasil ke Elasticsearch
95%summarizingMemicu proses ringkasan dokumen
100%doneSelesai

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, pakai python-docx + reportlab).
  • merge_utils.merge_pdfs_to_tempfile — menggabungkan beberapa PDF (lewat pypdf.PdfWriter) menjadi satu file, dipakai oleh POST /api/v1/pdfs/merge. File/URL non-PDF dikonversi dulu lewat FileConverter sebelum digabung.
  • merge_utils.download_to_tempfile — mengunduh file dari URL ke file sementara (dipakai /pdfs/merge untuk input url_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):

json
{
  "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:

HasilIndexKeterangan
final_summaryINDEX_DOCUMENT_SUMMARIZERingkasan penuh dokumen, id dokumen = task_id
summarize_chunk (list)INDEX_DOCUMENT_SUMMARIZE_CHUNKRingkasan 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)

ModeKeterangan
ekstrak_only (default)Ekstraksi teks saja, tanpa analisis gambar
representasiTeks + 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

bash
# 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