Lewati ke konten utama

Tutorial DeepSeek OCR-2: Membangun Aplikasi Tanya Jawab PDF “Sambil Mengindeks” dengan Gradio

Pelajari cara menjalankan DeepSeek OCR-2 secara lokal dengan vLLM dan membangun sistem tanya jawab PDF inkremental yang melakukan OCR mini-batch, pencarian berbasis FAISS, dan kueri waktu nyata dengan antarmuka Gradio.
Diperbarui 28 Sep 2026  · 13 mnt Baca

Jelajahi bersama AI

ChatGPTClaudePerplexity

DeepSeek OCR-2 dirancang untuk pemahaman dokumen yang berfokus pada visi, sehingga sangat efektif untuk PDF hasil pemindaian atau yang sarat gambar, di mana ekstraksi teks tradisional sering kesulitan. Dalam tutorial ini, kita akan menggunakan DeepSeek OCR-2 yang disajikan melalui vLLM untuk membangun asisten dokumen ringan “Tanya Sambil Mengindeks” dengan Gradio.

Di aplikasi ini, pengguna mengunggah PDF; setiap halaman dirender menjadi gambar berkualitas tinggi, dan OCR dijalankan dalam mini-batch. Teks yang diekstraksi diindeks secara inkremental ke dalam penyimpanan FAISS menggunakan SentenceTransformers, sehingga dokumen menjadi dapat dicari seiring pemrosesan berlangsung. Alih-alih menunggu seluruh berkas selesai, pengguna dapat mulai mengajukan pertanyaan setelah batch pertama diindeks.

Hasilnya adalah alur kerja chat dokumen yang praktis, menekankan waktu ke jawaban pertama yang cepat, pemanfaatan GPU yang efisien, dan pengalaman mirip streaming.

Saya juga merekomendasikan untuk melihat tutorial dasar DeepSeek OCR dan tutorial DeepSeek-V3.2-Speciale.

Apa itu DeepSeek OCR 2?

DeepSeek-OCR 2 adalah model visi-bahasa end-to-end untuk pembacaan dokumen sekaligus OCR yang dirancang untuk menangani tata letak kompleks (halaman multikolom, formulir, tabel, rumus) dengan mempelajari urutan baca yang lebih baik alih-alih meratakan potongan gambar dalam pemindaian raster tetap dari kiri-atas ke kanan-bawah. 

Gagasan intinya adalah Visual Causal Flow, yaitu model berusaha meniru cara manusia membaca dokumen, secara progresif dan semantik, bukan memperlakukan halaman seperti grid yang seragam. Secara arsitektural, DeepSeek-OCR 2 mempertahankan kerangka encoder–decoder seperti DeepSeek-OCR, tetapi meningkatkan encoder menjadi DeepEncoder V2, yang memperkenalkan mekanisme penyusunan ulang token kausal sebelum decoder menghasilkan teks.

Bagaimana DeepSeek OCR-2 bekerja?

Secara garis besar, DeepSeek OCR-2 mengonversi dokumen menjadi teks melalui pipeline visi-bahasa yang mempelajari urutan baca alih-alih mengandalkan pengurutan koordinat tetap. Ini mencakup:

Visual Causal Flow

Gambar: DeepSeek-OCR 2: Visual Causal Flow

  • Pengodean halaman multi-skala: Setiap halaman PDF dirender menjadi tampilan global dan potongan lokal beresolusi tinggi opsional. Gambar-gambar ini diubah menjadi token patch visual, memungkinkan model menangkap tata letak keseluruhan (bagian, kolom) dan detail halus seperti teks kecil, tabel, dan rumus.
  • Pemahaman tata letak dengan perhatian bidireksional: Token visual terlebih dahulu diproses dengan attention non-kausal (bidireksional) di dalam DeepEncoder V2, sehingga model memahami hubungan spasial seperti struktur kolom, batas tabel, dan pengelompokan wilayah.
  • Visual Causal Flow (urutan baca yang dipelajari): Encoder menambahkan token kueri kausal yang dapat dipelajari yang secara berurutan memperhatikan fitur visual dan kueri sebelumnya. Mekanisme ini mengekstrak konten dalam urutan baca yang dipelajari, membuat model tangguh untuk halaman multikolom, formulir, dan tata letak kompleks.
  • Generasi teks: Representasi visual yang sudah diurutkan diteruskan ke decoder bahasa, yang menghasilkan teks dokumen secara autoregresif dalam urutan logis yang benar.
  • Pemahaman dokumen end-to-end: Tidak seperti pipeline OCR tradisional, DeepSeek OCR-2 melakukan pemahaman tata letak dan transkripsi secara bersama-sama, menghasilkan teks yang lebih bersih dan struktur yang lebih andal untuk tugas hilir seperti pencarian, peringkasan, dan RAG.

Anda dapat merujuk ke repositori GitHub DeepSeek OCR-2 untuk menelusuri kodenya secara detail.

Tutorial DeepSeek OCR-2: Bangun Aplikasi Tanya Jawab PDF “Sambil Mengindeks”

Pada bagian ini, kita akan membangun aplikasi intelijen dokumen waktu nyata yang memungkinkan Anda mengunggah PDF, mengekstrak kontennya menggunakan DeepSeek OCR-2, dan mulai mengajukan pertanyaan saat dokumen masih diproses. Alih-alih menunggu OCR penuh selesai, halaman menjadi dapat dicari per batch, menghadirkan pengalaman yang jauh lebih cepat dan interaktif.

Secara garis besar, sistem melakukan empat tugas inti:

  • Mengonversi setiap halaman PDF menjadi gambar berkualitas tinggi
  • Menjalankan DeepSeek OCR-2 melalui vLLM untuk mengekstrak teks terstruktur
  • Mengindeks konten yang diekstraksi secara inkremental menggunakan FAISS dan pipeline RAG berbasis embedding
  • Memungkinkan pengguna mengkueri dokumen secara waktu nyata saat halaman diindeks

Aplikasi ini menghasilkan dua keluaran utama:

  • Konten dokumen yang dapat dicari dengan pengambilan tingkat halaman
  • Jawaban yang berlandaskan sumber yang menampilkan nomor halaman relevan dan cuplikan teks

Catatan: Tutorial ini dibangun di atas kode demo resmi OCR-2 vLLM milik DeepSeek, termasuk pembantu perenderan PDF, pengemasan input multimodal, pembersihan teks OCR, dan konfigurasi engine/sampling vLLM. Di atas itu, saya menambahkan lapisan RAG ringan, pemrosesan OCR mini-batch untuk memungkinkan pengindeksan inkremental, dan aplikasi Gradio.

Langkah 1: Penyiapan lingkungan

Sebelum menjalankan DeepSeek OCR-2 dengan vLLM, kita perlu memastikan lingkungan runtime siap. Dalam tutorial ini, kita mengasumsikan lingkungan berkemampuan GPU seperti Google Colab dengan A100 atau T4, karena vLLM dan DeepSeek OCR-2 sama-sama memerlukan CUDA untuk inferensi yang efisien.

Pada langkah ini, kita akan:

  • Memverifikasi bahwa GPU tersedia dan memeriksa kebutuhan memorinya
  • Mengklon repositori DeepSeek OCR-2 
  • Memastikan direktori integrasi vLLM tersedia 

Ini memastikan sisa pipeline dapat memuat model dan utilitas pendukung tanpa masalah path atau dependensi.

import subprocess
gpu = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'],
                     capture_output=True, text=True).stdout.strip()
print(f"GPU: {gpu}")
import os, sys
REPO_ROOT = '/content/DeepSeek-OCR-2-main'
VLLM_DIR  = f'{REPO_ROOT}/DeepSeek-OCR2-master/DeepSeek-OCR2-vllm'
if not os.path.isdir(VLLM_DIR):
    zip_candidates = [
        '/content/DeepSeek-OCR-2-main.zip',
    ]
    found_zip = None
    for zp in zip_candidates:
        if os.path.isfile(zp):
            found_zip = zp
            break
    if found_zip:
        !unzip -qo {found_zip} -d /content/
        print(f'Extracted {found_zip}')
    else:
        !git clone https://github.com/deepseek-ai/DeepSeek-OCR-2.git {REPO_ROOT} 2>/dev/null || echo 'Already cloned'
assert os.path.isdir(VLLM_DIR), f'Repo not found at {VLLM_DIR}'
print(f'Repo OK: {VLLM_DIR}')

Langkah ini memverifikasi bahwa GPU tersedia lalu menyiapkan basis kode DeepSeek OCR-2 secara lokal. Skrip di atas terlebih dahulu memeriksa GPU dan memori yang tersedia menggunakan nvidia-smi untuk memastikan lingkungan memiliki VRAM yang cukup untuk inferensi vLLM. 

Kemudian, mendefinisikan path repositori dan memeriksa apakah direktori integrasi vLLM yang diperlukan sudah ada. Jika tidak, skrip mengekstrak repositori dari ZIP lokal atau mengklon repositori resmi DeepSeek OCR-2 dari GitHub. 

Pada langkah berikutnya, kita akan menginstal dependensi yang dibutuhkan dan menyiapkan runtime untuk inferensi berbasis vLLM.

Langkah 2: Menginstal dependensi 

DeepSeek OCR-2 dan vLLM sangat sensitif terhadap versi, jadi pada langkah ini, kita menyematkan serangkaian paket yang berfungsi untuk CUDA 11.8, memasang wheel vLLM yang sesuai, dan menambahkan flash-attn untuk kernel attention yang lebih cepat. 

!pip install torch==2.6.0 torchvision==0.21.0 torchaudio==2.6.0 \
    --index-url https://download.pytorch.org/whl/cu118 2>&1 | tail -3
!pip install https://github.com/vllm-project/vllm/releases/download/v0.8.5/vllm-0.8.5+cu118-cp38-abi3-manylinux1_x86_64.whl 2>&1 | tail -3
!pip install flash-attn==2.7.3 --no-build-isolation 2>&1 | tail -3
!pip install "pydantic==2.10.6" "gradio==5.23.0" "gradio_client==1.7.2"
!pip install transformers==4.46.3 tokenizers==0.20.3 2>&1 | tail -3
!pip install addict einops tiktoken easydict pymupdf img2pdf pillow 2>&1 | tail -3
!pip install sentence-transformers faiss-cpu gradio 2>&1 | tail -3
os.environ['VLLM_USE_V1'] = '0'
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
import torch
if torch.version.cuda == '11.8':
    os.environ['TRITON_PTXAS_PATH'] = '/usr/local/cuda-11.8/bin/ptxas'
if VLLM_DIR not in sys.path:
    sys.path.insert(0, VLLM_DIR)
import os, sys, re, io, time, threading
import numpy as np
import faiss
import fitz
import gradio as gr
from PIL import Image
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from sentence_transformers import SentenceTransformer
VLLM_DIR = '/content/DeepSeek-OCR-2-main/DeepSeek-OCR2-master/DeepSeek-OCR2-vllm'
if VLLM_DIR not in sys.path:
    sys.path.insert(0, VLLM_DIR)
import config
from vllm import LLM, SamplingParams
from vllm.model_executor.models.registry import ModelRegistry
from process.ngram_norepeat import NoRepeatNGramLogitsProcessor
from process.image_process import DeepseekOCR2Processor
from deepseek_ocr2 import DeepseekOCR2ForCausalLM
print('Loading embedding model...')
EMBED_MODEL = SentenceTransformer('all-MiniLM-L6-v2')
EMBED_DIM = EMBED_MODEL.get_sentence_embedding_dimension()
print(f'Embedding model ready (dim={EMBED_DIM})')

Cuplikan kode di atas memasang runtime yang kompatibel untuk DeepSeek OCR-2 dengan vLLM. Kode tersebut juga memasang tumpukan pendukung aplikasi, termasuk Transformers, Gradio untuk UI, PyMuPDF dan Pillow untuk pemrosesan PDF/gambar, dan SentenceTransformers dengan FAISS untuk membangun indeks retrieval. 

Setelah instalasi, variabel lingkungan dikonfigurasi untuk eksekusi vLLM yang stabil dan kompatibilitas Triton. Integrasi vLLM repositori ditambahkan ke sys.path agar model DeepSeek OCR-2 kustom dan modul prapemrosesan dapat diimpor, dan akhirnya, model embedding ringan (all-MiniLM-L6-v2) dimuat untuk mendukung pengindeksan RAG tingkat halaman secara inkremental.

Pada langkah berikutnya, kita akan menerapkan utilitas prapemrosesan untuk mengonversi PDF menjadi gambar halaman, mengemas setiap halaman ke format input multimodal yang diharapkan oleh DeepSeek OCR-2, dan membersihkan keluaran OCR untuk retrieval.

Langkah 3: Utilitas prapemrosesan

Sebelum kita dapat menjalankan DeepSeek OCR-2, kita memerlukan serangkaian kecil utilitas prapemrosesan yang mengonversi PDF menjadi input siap model dan membersihkan keluaran OCR mentah untuk retrieval. 

BATCH_SIZE = 5
def pdf_to_images_high_quality(pdf_path, dpi=144, image_format='PNG'):
    images = []
    pdf_document = fitz.open(pdf_path)
    zoom = dpi / 72.0
    matrix = fitz.Matrix(zoom, zoom)
    for page_num in range(pdf_document.page_count):
        page = pdf_document[page_num]
        pixmap = page.get_pixmap(matrix=matrix, alpha=False)
        Image.MAX_IMAGE_PIXELS = None
        if image_format.upper() == 'PNG':
            img_data = pixmap.tobytes('png')
            img = Image.open(io.BytesIO(img_data))
        else:
            img_data = pixmap.tobytes('png')
            img = Image.open(io.BytesIO(img_data))
            if img.mode in ('RGBA', 'LA'):
                background = Image.new('RGB', img.size, (255, 255, 255))
                background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
                img = background
        images.append(img)
    pdf_document.close()
    return images
def process_single_image(image, prompt_str):
    return {
        'prompt': prompt_str,
        'multi_modal_data': {
            'image': DeepseekOCR2Processor().tokenize_with_images(
                images=[image], bos=True, eos=True, cropping=config.CROP_MODE
            )
        },
    }
def clean_ocr_output(content):
    if '<|end▁of▁sentence|>' in content:
        content = content.replace('<|end▁of▁sentence|>', '')
    else:
        if config.SKIP_REPEAT:
            return ''
    content = re.sub(r'<\|ref\|>image<\|/ref\|><\|det\|>.*?<\|/det\|>', '', content)
    content = re.sub(r'<\|ref\|>.*?<\|/ref\|><\|det\|>.*?<\|/det\|>', '', content)
    content = content.replace('\\coloneqq', ':=').replace('\\eqqcolon', '=:')
    content = content.replace('\n\n\n\n', '\n\n').replace('\n\n\n', '\n\n')
    return content.strip()

Utilitas di atas mendefinisikan tiga komponen kunci:

Batching untuk pencarian inkremental (perilaku “tanya sambil mengindeks”)

Kita menetapkan BATCH_SIZE = 5 untuk mengendalikan seberapa cepat hasil pertama muncul dibandingkan efisiensi penggunaan GPU. Untuk PDF 20 halaman, progres diperbarui bertahap dari 0% ke 25% ke 50% ke 75% ke 100%, alih-alih melompat langsung dari 0% ke 100%. 

Ini memungkinkan pengguna mulai bertanya segera setelah 5 halaman pertama diindeks. Batch yang lebih kecil mempercepat waktu ke hasil pertama, sementara batch lebih besar meningkatkan throughput dan pemanfaatan GPU.

Perenderan PDF berkualitas tinggi ke gambar

Kami menggunakan kode fungsi pdf_to_images_high_quality() asli dari implementasi awal menggunakan pustaka PyMuPDF untuk merender setiap halaman menjadi gambar PIL pada 144 DPI (berasal dari zoom = 144 / 72.0 = 2.0, yang menggandakan resolusi default 72 DPI). 

Matriks zoom dpi/72 mengontrol kualitas rasterisasi, dan alpha=False menghindari artefak transparansi. Jika gambar masih memiliki saluran alfa, fungsi akan meratakannya ke latar belakang putih untuk mencegah blending aneh yang dapat menurunkan kualitas OCR.

Input multimodal

Fungsi process_single_image() (dari basis kode asli) membungkus setiap gambar halaman ke dalam struktur dict yang persis diharapkan vLLM (prompt + multi_modal_data.image), menggunakan DeepseekOCR2Processor().tokenize_with_images() yang menangani seluruh pipeline prapemrosesan gambar termasuk pemecahan gambar besar menjadi 2–6 tile melalui dynamic_preprocess() berdasarkan rasio aspek, membuat tampilan global 1024×1024 yang dipadding, menerapkan normalisasi tensor, dan menghasilkan urutan token lengkap dengan mask gambar yang dikonsumsi DeepseekOCR2ForCausalLM. 

Pembersihan OCR

Parameter config.CROP_MODE pada repo mengontrol apakah tiling dinamis ini diterapkan, yang meningkatkan akurasi pada tata letak padat dengan teks kecil. 

Setelah generasi, clean_ocr_output() menghilangkan token akhir kalimat khusus, membuang blok markup deteksi (<|ref|>...<|/ref|><|det|>...<|/det|>), menormalkan beberapa operator mirip LaTeX, dan merapikan jeda baris berlebih, menghasilkan teks yang lebih bersih sehingga lebih baik dipotong dan lebih andal diambil kembali dalam indeks RAG hilir.

Berikutnya, kita akan menggunakan keluaran yang sudah dibersihkan ini untuk membangun indeks RAG ringan tingkat halaman, sehingga halaman menjadi dapat dicari segera setelah setiap batch selesai.

Langkah 4: Engine RAG

Sekarang kita dapat mengekstrak teks bersih dari setiap halaman, langkah berikutnya adalah membuatnya segera dapat dicari. Alih-alih menunggu seluruh dokumen selesai diproses, kita membangun engine Retrieval-Augmented Generation (RAG) tingkat halaman yang mengindeks konten secara inkremental. 

Saat setiap batch halaman diproses, teksnya di-embed dan ditambahkan ke indeks FAISS, memungkinkan pengguna mengkueri dokumen saat pengindeksan masih berlangsung.

class PageRAG:
    def __init__(self, chunk_size=1000):
        self.chunk_size = chunk_size
        self.index = faiss.IndexFlatIP(EMBED_DIM)
        self.chunks = []
        self.page_count = 0
        self._lock = threading.Lock()
    def reset(self):
        with self._lock:
            self.index = faiss.IndexFlatIP(EMBED_DIM)
            self.chunks, self.page_count = [], 0
    def add_page(self, page_num, text):
        if not text or not text.strip():
            with self._lock: self.page_count += 1
            return
        new_chunks = self._split(text, page_num)
        texts = [c[1] for c in new_chunks]
        embs = EMBED_MODEL.encode(texts, normalize_embeddings=True)
        with self._lock:
            self.index.add(np.array(embs, dtype=np.float32))
            self.chunks.extend(new_chunks)
            self.page_count += 1
    def query(self, question, top_k=5):
        with self._lock:
            if self.index.ntotal == 0: return []
            q = EMBED_MODEL.encode([question], normalize_embeddings=True)
            k = min(top_k, self.index.ntotal)
            scores, ids = self.index.search(np.array(q, dtype=np.float32), k)
        return [{'page': self.chunks[i][0], 'score': float(s), 'text': self.chunks[i][1]}
                for s, i in zip(scores[0], ids[0]) if i >= 0]
    def _split(self, text, pn):
        if len(text) <= self.chunk_size: return [(pn, text)]
        parts, cur = [], ''
        for p in re.split(r'\n{2,}', text):
            if len(cur) + len(p) > self.chunk_size and cur:
                parts.append((pn, cur.strip())); cur = p
            else: cur += ('\n\n' + p if cur else p)
        if cur.strip(): parts.append((pn, cur.strip()))
        return parts or [(pn, text[:self.chunk_size])]
    @property
    def indexed_pages(self):
        with self._lock: return self.page_count

Kelas di atas mendefinisikan empat komponen kunci:

  • Pengindeksan vektor dengan FAISS: Engine menggunakan faiss.IndexFlatIP untuk pencarian kemiripan cepat atas embedding yang dinormalisasi. Saat setiap halaman diproses, kontennya langsung di-embed dan ditambahkan ke indeks, memungkinkan retrieval waktu nyata tanpa membangun ulang indeks.
  • Chunking tingkat halaman: Metode _split() memecah teks halaman panjang menjadi chunk ~1000 karakter berdasarkan batas paragraf. Ini meningkatkan pencocokan semantik dan menghindari embedding konteks yang terlalu panjang dan berisik sambil tetap mempertahankan nomor halaman untuk atribusi sumber.
  • Pembaruan untuk pemrosesan latar belakang: Karena OCR dan pengindeksan berjalan di thread latar belakang sementara pengguna mungkin mengkueri secara bersamaan, threading.Lock() melindungi pembaruan dan pembacaan indeks. 
  • Pencarian semantik cepat dengan landasan sumber: Metode query() melakukan embedding pertanyaan pengguna, mengambil top-k chunk serupa, dan mengembalikan teks yang cocok beserta nomor halaman dan skor kemiripan. Properti indexed_pages melacak progres sehingga UI dapat menampilkan berapa banyak halaman yang sudah dapat dicari.

Langkah 5: Engine vLLM

Pada langkah ini, kita menginisialisasi engine inferensi DeepSeek OCR-2 menggunakan vLLM. Ini adalah komponen inti yang melakukan generasi multimodal dengan mengambil gambar halaman yang sudah ditokenisasi dan menghasilkan teks OCR terstruktur. Konfigurasi mengikuti pengaturan referensi vLLM DeepSeek OCR-2 resmi, tetapi kita membungkusnya dalam pemuat malas load_ocr_engine() sehingga model hanya dimuat saat pemrosesan dimulai, mengurangi waktu startup dan menghindari alokasi GPU yang tidak perlu.

ocr_llm = None
ocr_sampling_params = None
def load_ocr_engine():
    global ocr_llm, ocr_sampling_params
    if ocr_llm is not None:
        return
    print(f'Loading {config.MODEL_PATH} via vLLM...')
    ModelRegistry.register_model('DeepseekOCR2ForCausalLM', DeepseekOCR2ForCausalLM)
    ocr_llm = LLM(
        model=config.MODEL_PATH,
        hf_overrides={'architectures': ['DeepseekOCR2ForCausalLM']},
        block_size=256,
        enforce_eager=False,
        trust_remote_code=True,
        max_model_len=8192,
        swap_space=0,
        max_num_seqs=config.MAX_CONCURRENCY,
        tensor_parallel_size=1,
        gpu_memory_utilization=0.9,
        disable_mm_preprocessor_cache=True,
    )
    ocr_sampling_params = SamplingParams(
        temperature=0.0,
        max_tokens=8192,
        logits_processors=[
            NoRepeatNGramLogitsProcessor(
                ngram_size=20, window_size=50,
                whitelist_token_ids={128821, 128822},
            )
        ],
        skip_special_tokens=False,
        include_stop_str_in_output=True,
    )
    print('vLLM engine loaded')

Cuplikan kode ini mendefinisikan komponen inti berikut:

  • Pendaftaran model: Pemanggilan ModelRegistry.register_model() menghubungkan kelas kustom DeepseekOCR2ForCausalLM dari repositori ke vLLM agar dapat menangani input token gambar yang dihasilkan oleh DeepseekOCR2Processor.
  • Konfigurasi engine vLLM: Parameter LLM() sesuai dengan pengaturan referensi repositori, termasuk block_size=256, max_model_len=8192, gpu_memory_utilization=0.9, dan disable_mm_preprocessor_cache=True. Pengaturan ini disetel untuk inferensi OCR ber-throughput tinggi yang stabil sambil menjaga penggunaan memori GPU tetap terprediksi.
  • Decoding terkontrol: SamplingParams identik dengan konfigurasi asli, yaitu decoding deterministik temperature=0.0, batas keluaran besar, dan NoRepeatNGramLogitsProcessor untuk mencegah pola teks berulang, yang umum terjadi pada generasi OCR bentuk panjang.
  • Pemrosesan malas (lazy loading): Alih-alih menginisialisasi model saat impor, engine dimuat sekali saja ketika load_ocr_engine() pertama kali dipanggil. Pemanggilan berikutnya menggunakan instance yang sama, sehingga menghindari inisialisasi GPU berulang dan menjaga UI tetap responsif.

Sekarang, mari hubungkan engine ini ke loop pemrosesan batch yang menjalankan OCR per halaman dan mengalirkan hasilnya ke indeks RAG untuk kueri waktu nyata.

Langkah 6: Pipeline “Tanya Sambil Mengindeks”

Langkah ini adalah inti aplikasi. Kami menerapkan pekerja latar belakang yang mengambil PDF yang diunggah dan secara bertahap mengubahnya menjadi indeks yang dapat dicari, alih-alih menunggu seluruh dokumen selesai OCR. 

rag = PageRAG()
total_pages = 0
is_processing = False
processing_error = None
stop_requested = False
def process_pdf_background(pdf_path):
    global total_pages, is_processing, processing_error, stop_requested
    try:
        is_processing = True
        processing_error = None
        stop_requested = False
        load_ocr_engine()
        if stop_requested: return
        print('PDF loading...')
        images = pdf_to_images_high_quality(pdf_path)
        total_pages = len(images)
        print(f'Rendered {total_pages} pages.')
        if stop_requested: return
        print('Preprocessing all images with DeepseekOCR2Processor...')
        prompt = config.PROMPT
        preprocess_fn = partial(process_single_image, prompt_str=prompt)
        with ThreadPoolExecutor(max_workers=config.NUM_WORKERS) as executor:
            all_inputs = list(executor.map(preprocess_fn, images))
        print(f'Preprocessed {len(all_inputs)} pages.')
        if stop_requested: return
        num_batches = (total_pages + BATCH_SIZE - 1) // BATCH_SIZE
        print(f'Running inference in {num_batches} batches of up to {BATCH_SIZE} pages...')
        for batch_idx in range(num_batches):
            if stop_requested:
                print(f'Stopped before batch {batch_idx + 1}.')
                return
            start = batch_idx * BATCH_SIZE
            end = min(start + BATCH_SIZE, total_pages)
            batch_inputs = all_inputs[start:end]
            print(f'  Batch {batch_idx + 1}/{num_batches}: pages {start+1}-{end}...')
            outputs_list = ocr_llm.generate(
                batch_inputs,
                sampling_params=ocr_sampling_params,
            )
            for i, output in enumerate(outputs_list):
                page_num = start + i + 1
                cleaned = clean_ocr_output(output.outputs[0].text)
                rag.add_page(page_num, cleaned)
                print(f'    Page {page_num}/{total_pages} ({len(cleaned)} chars)')
            print(f'  Batch {batch_idx + 1} done. {rag.indexed_pages} pages now searchable.')
    except Exception as e:
        import traceback
        processing_error = f'{e}\n\n{traceback.format_exc()}'
        print(f'ERROR: {processing_error}')
    finally:
        is_processing = False
        if stop_requested:
            print(f'Stopped. {rag.indexed_pages} pages indexed.')
        else:
            print('Processing complete.')

Alur pipeline kita mencakup perenderan halaman, prapemrosesan input, menjalankan inferensi vLLM dalam mini-batch, lalu membersihkan keluaran OCR, dan akhirnya menambahkan setiap halaman ke PageRAG yang didukung FAISS. Berikut analisis detail tiap langkah: 

  • Manajemen status: Variabel global (total_pages, is_processing, processing_error, stop_requested) melacak progres dan mode kegagalan sehingga UI dapat menampilkan status langsung, menangani stop/reset, dan menghindari proses yang saling bertentangan.
  • Prapemrosesan paralel: ThreadPoolExecutor(max_workers=config.NUM_WORKERS) mengonversi gambar halaman menjadi input multimodal siap model secara paralel. Ini menjaga GPU tetap sibuk nanti dengan memastikan input siap sebelum inferensi.
  • Inferensi mini-batch: Alih-alih menjalankan ocr_llm.generate() sekali untuk semua halaman (default skrip resmi), kode menghitung num_batches dan memproses halaman dalam potongan BATCH_SIZE. Ini memungkinkan pengindeksan progresif, sehingga pengguna dapat mulai bertanya setelah 5 halaman pertama (BATCH_SIZE = 5) diindeks, bukan menunggu seluruh dokumen.
  • Penghentian antar batch: Variabel stop_requested diperiksa sebelum tahap mahal dan di awal setiap batch. Ini membuat tombol Stop dapat diprediksi: proses berhenti di antara batch, setelah batch saat ini selesai, sehingga Anda tidak berakhir pada keadaan setengah tulis atau keluaran parsial.

Pada titik ini, kita memiliki pipeline end-to-end yang mengonversi PDF menjadi indeks pencarian yang terus bertambah, sehingga pengguna dapat mengkueri dokumen saat pemrosesan berlanjut di latar belakang. Selanjutnya, kita akan menambahkan callback UI yang menghubungkan pekerja ini ke aplikasi Gradio.

Langkah 7: Callback aplikasi

Pada tahap ini, pipeline OCR dan pengindeksan berjalan di latar belakang, tetapi aplikasi masih memerlukan lapisan kontrol yang menghubungkan aksi pengguna ke logika pemrosesan. 

def start_processing(pdf_file):
    global total_pages
    if pdf_file is None: return 'Upload a PDF first.'
    if is_processing: return 'Already processing.'
    rag.reset()
    total_pages = 0
    threading.Thread(
        target=process_pdf_background,
        args=(pdf_file.name,),
        daemon=True,
    ).start()
    return 'Processing started! Pages become searchable in batches.'
def stop_processing():
    global stop_requested
    if not is_processing:
        return 'Nothing is running.'
    stop_requested = True
    return 'Stop requested. Will halt after current batch.'
def reset_all():
    global total_pages, processing_error, stop_requested
    if is_processing:
        return 'Cannot reset while processing. Stop first.'
    rag.reset()
    total_pages = 0
    processing_error = None
    stop_requested = False
    return 'Reset complete. Upload a new PDF.'
def refresh_progress():
    idx, tp = rag.indexed_pages, total_pages
    if processing_error:
        msg, pct, c = f'Error: {processing_error[:200]}', 100, '#ef4444'
    elif stop_requested and is_processing:
        pct = int(idx / tp * 100) if tp > 0 else 0
        msg, c = 'Stopping after current batch...', '#f59e0b'
    elif not is_processing and stop_requested:
        pct = int(idx / tp * 100) if tp > 0 else 0
        msg, c = f'Stopped. {idx} pages indexed -- you can still ask questions.', '#f59e0b'
    elif not is_processing and idx == 0:
        msg, pct, c = 'Upload a PDF and click Process.', 0, '#6b7280'
    elif is_processing and tp == 0:
        msg, pct, c = 'Loading vLLM engine & rendering pages...', 0, '#3b82f6'
    elif is_processing:
        pct = int(idx / tp * 100)
        msg, c = f'{idx}/{tp} pages indexed -- ask questions now!', '#3b82f6'
    else:
        msg, pct, c = f'All {idx} pages indexed.', 100, '#10b981'
    bar_min = '32px' if pct > 0 else '0'
    pct_text = f'{pct}%' if pct > 5 else ''
    return (f'<div style=">'
            f'<div style="font-weight:500">{msg}</div>'
            f'<div style="background:#1f2937;border-radius:10px;overflow:hidden;height:28px">'
            f'<div style="width:{pct}%;background:{c};height:100%;border-radius:10px;'
            f'transition:width .6s;display:flex;align-items:center;'
            f'justify-content:center;color:white;font-weight:600;'
            f'min-width:{bar_min}">{pct_text}</div></div></div>')
def answer_question(question):
    if not question.strip(): return ''
    idx = rag.indexed_pages
    if idx == 0:
        if is_processing:
            return ('**Processing in progress -- first batch not done yet.**\n\n'
                    'Pages become searchable in batches of ' + str(BATCH_SIZE) + '.\n'
                    'Try again shortly!')
        return 'Upload a PDF and click **Process** first.'
    results = rag.query(question, top_k=5)
    if not results: return 'No relevant results found.'
    lines = []
    if is_processing:
        lines.append(f'> *Searched {idx} of {total_pages} pages (still processing).*\n')
    lines.append(f'**Source pages: {list(dict.fromkeys(r["page"] for r in results))}**\n')
    for i, r in enumerate(results, 1):
        lines += ['---', f'**Result {i}** | Page {r["page"]} | score: {r["score"]:.3f}\n',
                  r['text'][:600]]
        if len(r['text']) > 600: lines.append(f'\n*...{len(r["text"]):,} chars total*')
    return '\n'.join(lines)

Lapisan kontrol ini menangani lima tanggung jawab inti:

  • Memulai pemrosesan secara asinkron: Fungsi start_processing()  memvalidasi input, mereset indeks RAG, dan meluncurkan process_pdf_background() dalam thread daemon. 
  • Penghentian aman antar batch: Fungsi stop_processing() menetapkan flag stop_requested alih-alih segera menghentikan eksekusi. Pekerja latar belakang memeriksa flag ini di antara batch, memastikan sistem berhenti dengan bersih tanpa merusak indeks.
  • Reset status: Fungsi reset_all() ini membersihkan indeks FAISS, penghitung progres, dan status error. Fungsi ini juga mencegah reset tidak sengaja saat pemrosesan masih aktif, yang melindungi dari keadaan aplikasi yang tidak konsisten.
  • Pelaporan progres waktu nyata: Kita menggunakan fungsi refresh_progress() untuk membaca jumlah halaman yang diindeks dari PageRAG dan menghasilkan bilah progres HTML dinamis. Fungsi ini juga mengomunikasikan status sistem seperti memuat, memproses aktif, menghentikan, selesai, atau error.
  • Tanya jawab inkremental dengan landasan sumber: Terakhir, answer_question() melakukan kueri ke indeks FAISS dan mengembalikan chunk yang paling sesuai beserta nomor halaman dan skor kemiripan. Jika pengindeksan masih berjalan, respons secara eksplisit menunjukkan berapa banyak halaman yang sudah ditelusuri sejauh ini, menegaskan perilaku “tanya sambil mengindeks”.

Pada langkah terakhir, kita akan menghubungkan callback ini ke antarmuka Gradio untuk menampilkan alur kerja lengkap melalui UI web sederhana.

Langkah 8: UI Gradio

Pada langkah akhir ini, kita membungkus semuanya ke dalam aplikasi Gradio sederhana. UI dirancang agar pengguna dapat mengunggah PDF, memulai pemrosesan, dan mulai bertanya sebelum pengindeksan selesai. 

with gr.Blocks(title='DeepSeek OCR-2', theme=gr.themes.Soft()) as demo:
    gr.Markdown('# DeepSeek OCR-2 -- Ask While Indexing\n')
    with gr.Row(equal_height=True):
        with gr.Column(scale=1, min_width=250):
            pdf_input = gr.File(label='Upload PDF', file_types=['.pdf'])
            process_btn = gr.Button('Process PDF', variant='primary', size='lg')
            with gr.Row():
                stop_btn = gr.Button('Stop', variant='stop', size='sm')
                reset_btn = gr.Button('Reset', variant='secondary', size='sm')
            status_output = gr.Markdown()
        with gr.Column(scale=2):
            progress_bar = gr.HTML(
                value='<div style=">Upload a PDF and click Process.</div>')
    gr.Markdown('---')
    with gr.Row(equal_height=True):
        question_input = gr.Textbox(
            label='Ask a Question',
            placeholder='What is this document about?',
            lines=1, scale=4)
        ask_btn = gr.Button('Ask', variant='secondary', scale=1, min_width=100)
    answer_output = gr.Markdown()
    timer = gr.Timer(2)
    timer.tick(fn=refresh_progress, outputs=progress_bar)
    process_btn.click(fn=start_processing, inputs=pdf_input, outputs=status_output)
    stop_btn.click(fn=stop_processing, outputs=status_output)
    reset_btn.click(fn=reset_all, outputs=status_output)
    ask_btn.click(fn=answer_question, inputs=question_input, outputs=answer_output)
    question_input.submit(fn=answer_question, inputs=question_input, outputs=answer_output)
print('Launching demo...')
demo.queue()
demo.launch(debug=True, share=True)

Aplikasi Gradio berisi dua kolom. Kolom kiri berisi widget unggah PDF dan tombol Process/Stop/Reset, sementara kolom kanan menampilkan bilah progres HTML langsung. Ini membuat aplikasi terasa responsif bahkan saat OCR masih berjalan. Berikut beberapa fitur kunci aplikasi kita:

  • Indikator progres berbasis polling: gr.Timer(2) memanggil refresh_progress() setiap dua detik dan memperbarui HTML bilah progres. Karena pengindeksan terjadi per batch, pengguna melihat progres bergerak bertahap (dan pesan berubah berdasarkan status seperti memuat, menghentikan, error, atau selesai).
  • Pengkabelan event ke callback: Handler tombol .click() menghubungkan aksi UI langsung ke fungsi backend, misalnya start_processing() meluncurkan pekerja latar belakang, stop_processing() meminta penghentian bersih, reset_all() menghapus status, dan answer_question() menjalankan retrieval atas halaman yang sudah diindeks sejauh ini.
  • Interaksi Tanya Jawab: Pertanyaan dapat dikirim dengan mengklik Ask atau menekan Enter. Jawaban dirender sebagai Markdown, yang bekerja cukup baik untuk teks tetapi kurang tepat untuk teks yang mengandung kode, persamaan, atau karakter unik.

Terakhir, demo.launch() meluncurkan aplikasi di Colab, menyediakan tautan yang dapat dibagikan, merendernya secara inline, dan mengaktifkan log debug untuk membantu mendiagnosis masalah selama pengembangan.

Demo final

Kesimpulan

Dalam tutorial ini, kita membangun pipeline intelijen dokumen berfokus visi yang lengkap menggunakan DeepSeek OCR-2. Dimulai dari PDF mentah, kita merender halaman menjadi gambar berkualitas tinggi, menjalankan OCR multimodal melalui vLLM, membersihkan dan menstrukturkan teks yang diekstraksi, serta mengindeksnya secara inkremental ke dalam penyimpanan RAG berbasis FAISS. 

Dengan memproses halaman dalam mini-batch, aplikasi menghadirkan pengalaman “tanya sambil mengindeks” di mana pengguna dapat mulai mengkueri dokumen setelah batch pertama, alih-alih menunggu hingga selesai sepenuhnya.

Namun, dua keterbatasan mencolok muncul selama pengujian. Wilayah yang banyak rumus tidak selalu ditranskripsikan dengan akurat, yang merupakan tantangan umum bagi model OCR visi-bahasa, bahkan dengan prompt berbasis grounding. 

Selain itu, beberapa halaman yang lebih panjang menunjukkan pola keluaran berulang. Pipeline resmi mengurangi hal ini dengan NoRepeatNGramLogitsProcessor (ngram_size=20), tetapi tabel padat dan tata letak multikolom masih dapat memicu loop generasi; dalam kasus tersebut, halaman dilewati secara senyap melalui flag SKIP_REPEAT.

Untuk melangkah lebih jauh, pembaca dapat bereksperimen dengan menambahkan lapisan LLM ringan (misalnya, memanggil API Anthropic atau OpenAI) untuk meringkas chunk yang diambil alih-alih mengembalikan potongan mentah. 

Peningkatan praktis lainnya termasuk menyimpan indeks FAISS ke disk untuk digunakan kembali lintas sesi, mendukung beberapa PDF dalam satu indeks, mengekspos parameter yang dapat diatur seperti BATCH_SIZE dan top_k langsung di UI Gradio, atau menambahkan chunking yang sadar struktur (judul, tabel, dan batas bagian) untuk meningkatkan presisi retrieval.

FAQs

Apakah ini menggunakan pipeline resmi DeepSeek OCR-2?

Ya. Backend mencerminkan pendekatan DeepSeek-OCR2-vllm dari repo, termasuk prapemrosesan DeepseekOCR2Processor bersama DeepseekOCR2ForCausalLM yang didaftarkan ke vLLM dan fungsi LLM.generate().

Mengapa tidak menggunakan `AutoModel.from_pretrained()`?

Path AutoModel.from_pretrained() cenderung bermasalah karena ketidakcocokan transformers / flash-attn / CUDA ABI dan perubahan API internal. Jalur vLLM pada repo lebih stabil untuk alur kerja ini.

Bagaimana cara meningkatkan kualitas tabel?

Coba tingkatkan dpi (144 ke 168 atau 192), dan pertahankan crop_mode konsisten dengan konfigurasi repo. Perlu dicatat bahwa tabel sering sensitif terhadap resolusi.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Saya adalah Google Developers Expert di ML (Gen AI), Kaggle 3x Expert, dan Women Techmakers Ambassador dengan pengalaman lebih dari 3 tahun di bidang teknologi. Saya ikut mendirikan startup health-tech pada 2020 dan sedang menempuh studi magister ilmu komputer di Georgia Tech dengan spesialisasi machine learning.

Topik
Kecerdasan Buatan
Large Language Models

Kursus Terbaik di DataCamp

{count, plural, one {Kursus} other {Kursus}}

Bekerja dengan DeepSeek di Python

{hours} jam
1.4K
Temukan apa sebenarnya yang membuat DeepSeek begitu populer! Bangun aplikasi menggunakan model R1 dan V3 dari DeepSeek.
Lihat DetailRight Arrow
Mulai Kursus
Lihat SelengkapnyaRight Arrow
Terkait

{count, plural, one {blog} other {blogs}}

40 Pertanyaan Wawancara DBMS Teratas di 2026

Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Dario Radečić's photo

Dario Radečić

15 mnt

{count, plural, one {blog} other {blogs}}

12 Alternatif ChatGPT Terbaik yang Bisa Anda Coba pada 2026

Artikel ini menyajikan daftar alternatif ChatGPT yang akan meningkatkan produktivitas Anda.
Javier Canales Luna's photo

Javier Canales Luna

14 mnt

{count, plural, one {blog} other {blogs}}

Tutorial Korelasi di R

Dapatkan pengenalan dasar-dasar korelasi di R: pelajari lebih lanjut tentang koefisien korelasi, matriks korelasi, plotting korelasi, dan sebagainya.
David Woods's photo

David Woods

13 mnt

{count, plural, one {blog} other {blogs}}

Spaghetti Plot dan Jalur Badai

Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Hugo Bowne-Anderson's photo

Hugo Bowne-Anderson

13 mnt

Lihat SelengkapnyaLihat Selengkapnya