Pular para o conteúdo principal

Tutorial DeepSeek OCR-2: construindo um app de Q&A em PDF que permite perguntar enquanto indexa com Gradio

Aprenda a rodar o DeepSeek OCR-2 localmente com vLLM e a criar um sistema incremental de Q&A em PDF que faz OCR em minibatches, recuperação com FAISS e consultas em tempo real via interface Gradio.
Atualizado 17 de set. de 2026  · 13 min lido

Explorar com IA

ChatGPTClaudePerplexity

O DeepSeek OCR-2 foi criado para compreensão de documentos com foco em visão, sendo especialmente eficaz para PDFs escaneados ou com muitas imagens, onde a extração de texto tradicional costuma falhar. Neste tutorial, vamos usar o DeepSeek OCR-2 servido via vLLM para construir um assistente leve de documentos no estilo “pergunte enquanto indexa” usando Gradio.

No app, o usuário envia um PDF; cada página é renderizada em uma imagem de alta qualidade e o OCR é executado em minibatches. O texto extraído é indexado de forma incremental em um repositório FAISS usando SentenceTransformers, permitindo que o documento se torne pesquisável conforme o processamento avança. Em vez de esperar o arquivo inteiro terminar, o usuário já pode fazer perguntas assim que o primeiro lote for indexado.

O resultado é um fluxo prático de chat com documentos que prioriza menor tempo até a primeira resposta, uso eficiente de GPU e uma experiência parecida com streaming.

Também recomendo conferir nosso tutorial base do DeepSeek OCR e o tutorial DeepSeek-V3.2-Speciale.

O que é o DeepSeek OCR 2?

O DeepSeek-OCR 2 é um modelo visão–linguagem de leitura de documentos com OCR, fim a fim, projetado para lidar com layouts complexos (páginas com múltiplas colunas, formulários, tabelas, fórmulas) aprendendo uma ordem de leitura melhor, em vez de achatar blocos de imagem em uma varredura fixa do canto superior esquerdo para o inferior direito. 

A ideia central é o Visual Causal Flow, ou seja, o modelo tenta imitar como humanos leem documentos — de forma progressiva e semântica — em vez de tratar a página como uma grade uniforme. Na arquitetura, o DeepSeek-OCR 2 mantém o enquadramento codificador–decodificador do DeepSeek-OCR, mas atualiza o codificador para o DeepEncoder V2, que introduz um mecanismo causal de reordenação de tokens antes do decodificador gerar o texto.

Como o DeepSeek OCR-2 funciona?

Em alto nível, o DeepSeek OCR-2 converte um documento em texto por meio de um pipeline visão–linguagem que aprende a ordem de leitura, em vez de depender de uma ordenação fixa por coordenadas. Isso inclui:

Visual Causal Flow

Figura: DeepSeek-OCR 2: Visual Causal Flow

  • Codificação de página em múltiplas escalas: Cada página do PDF é renderizada em uma visão global e, opcionalmente, em recortes locais de alta resolução. Essas imagens são convertidas em tokens visuais (patches), permitindo que o modelo capture tanto o layout geral (seções, colunas) quanto detalhes finos, como textos pequenos, tabelas e fórmulas.
  • Compreensão de layout com atenção bidirecional: Os tokens visuais são primeiro processados com atenção não causal (bidirecional) dentro do DeepEncoder V2, permitindo que o modelo entenda relações espaciais como estrutura de colunas, limites de tabelas e agrupamento de regiões.
  • Visual Causal Flow (ordem de leitura aprendida): O codificador adiciona tokens de consulta causais aprendíveis que atentam sequencialmente às features visuais e às consultas anteriores. Esse mecanismo extrai conteúdo em uma sequência de leitura aprendida, tornando o modelo robusto a páginas multicoluna, formulários e layouts complexos.
  • Geração de texto: A representação visual ordenada é passada para um decodificador de linguagem, que gera o texto do documento de forma autoregressiva na ordem lógica correta.
  • Compreensão de documentos fim a fim: Diferente de pipelines de OCR tradicionais, o DeepSeek OCR-2 realiza entendimento de layout e transcrição em conjunto, produzindo texto mais limpo e uma estrutura mais confiável para tarefas posteriores como busca, sumarização e RAG.

Você pode consultar o repositório no GitHub do DeepSeek OCR-2 para explorar o código em detalhes.

Tutorial DeepSeek OCR-2: construa um app de Q&A em PDF que permite perguntar enquanto indexa

Nesta seção, vamos construir um aplicativo de inteligência de documentos em tempo real que permite enviar um PDF, extrair seu conteúdo com o DeepSeek OCR-2 e começar a fazer perguntas enquanto o documento ainda está sendo processado. Em vez de esperar o OCR completo, as páginas se tornam pesquisáveis lote a lote, oferecendo uma experiência muito mais rápida e interativa.

Em alto nível, o sistema executa quatro tarefas principais:

  • Converter cada página do PDF em imagens de alta qualidade
  • Executar o DeepSeek OCR-2 via vLLM para extrair texto estruturado
  • Indexar incrementalmente o conteúdo extraído usando uma FAISS e uma pipeline de RAG baseada em embeddings
  • Permitir que usuários consultem o documento em tempo real conforme as páginas são indexadas

O aplicativo gera duas saídas principais:

  • Conteúdo do documento pesquisável com recuperação por página
  • Respostas com lastro nas fontes mostrando números de página relevantes e trechos de texto

Observação: este tutorial parte do código de demo oficial do OCR-2 com vLLM do DeepSeek, incluindo o helper de renderização de PDF, empacotamento de entrada multimodal, limpeza do texto de OCR e a configuração do engine/sampling no vLLM. Em cima disso, adiciono uma camada leve de RAG, processamento de OCR em minibatches para permitir indexação incremental e um app em Gradio.

Passo 1: configuração do ambiente

Antes de rodar o DeepSeek OCR-2 com vLLM, precisamos garantir que o ambiente de execução está pronto. Neste tutorial, assumimos um ambiente com GPU, como o Google Colab com A100 ou T4, já que tanto o vLLM quanto o DeepSeek OCR-2 requerem CUDA para inferência eficiente.

Neste passo, vamos:

  • Verificar se há GPU disponível e checar os requisitos de memória
  • Clonar o repositório do DeepSeek OCR-2 
  • Confirmar se o diretório de integração com vLLM existe 

Isso garante que o restante do pipeline consiga carregar o modelo e os utilitários de apoio sem problemas de caminho ou dependências.

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}')

Este passo verifica se há uma GPU disponível e prepara localmente a base de código do DeepSeek OCR-2. O script acima primeiro checa a GPU e a memória disponíveis usando nvidia-smi para garantir VRAM suficiente para inferência com vLLM. 

Depois, define os caminhos do repositório e verifica se o diretório de integração com vLLM já existe. Caso não exista, ele extrai o repositório de um ZIP local ou clona o repositório oficial do DeepSeek OCR-2 no GitHub. 

No próximo passo, vamos instalar as dependências necessárias e preparar o runtime para inferência com vLLM.

Passo 2: instalar dependências 

DeepSeek OCR-2 e vLLM são bem sensíveis a versões. Então, neste passo, fixamos um conjunto funcional de pacotes para CUDA 11.8, instalamos um wheel do vLLM compatível e adicionamos flash-attn para kernels de atenção mais rápidos. 

!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})')

O trecho acima instala um runtime compatível para rodar o DeepSeek OCR-2 com vLLM. Ele também instala a stack de suporte do aplicativo, incluindo Transformers, Gradio para a UI, PyMuPDF e Pillow para processamento de PDF/imagem, e SentenceTransformers com FAISS para construir o índice de recuperação. 

Após a instalação, variáveis de ambiente são configuradas para uma execução estável do vLLM e compatibilidade com o Triton. A integração com vLLM do repositório é adicionada ao sys.path para que o modelo customizado do DeepSeek OCR-2 e os módulos de pré-processamento possam ser importados e, por fim, um modelo leve de embeddings (all-MiniLM-L6-v2) é carregado para dar suporte à indexação incremental por página no RAG.

No próximo passo, vamos implementar utilitários de pré-processamento para converter um PDF em imagens de página, empacotar cada página no formato multimodal esperado pelo DeepSeek OCR-2 e limpar as saídas de OCR para recuperação.

Passo 3: utilitários de pré-processamento

Antes de rodarmos o DeepSeek OCR-2, precisamos de um pequeno conjunto de utilitários de pré-processamento que convertam um PDF em entradas prontas para o modelo e limpem a saída bruta de OCR para recuperação. 

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()

Os utilitários acima definem três componentes-chave:

Loteamento para busca incremental (o comportamento “pergunte enquanto indexa”)

Definimos BATCH_SIZE = 5 para controlar quão rápido os primeiros resultados aparecem versus a eficiência de uso da GPU. Para um PDF de 20 páginas, o progresso avança em etapas de 0% para 25%, 50%, 75% e 100%, em vez de pular direto de 0% para 100%. 

Isso permite que os usuários comecem a perguntar assim que as primeiras 5 páginas forem indexadas. Lotes menores melhoram o tempo até o primeiro resultado, enquanto lotes maiores melhoram a vazão e a utilização da GPU.

Renderização de PDF em alta qualidade para imagens

Usamos o código nativo da função pdf_to_images_high_quality() da implementação original com a biblioteca PyMuPDF para renderizar cada página em uma imagem PIL a 144 DPI (deriva de zoom = 144 / 72.0 = 2.0, que dobra a resolução padrão de 72 DPI). 

A matriz de zoom dpi/72 controla a qualidade da rasterização e alpha=False evita artefatos de transparência. Se uma imagem ainda contiver canal alfa, a função achata sobre um fundo branco para prevenir mesclas estranhas que podem degradar o OCR.

Entradas multimodais

A função process_single_image() (do código original) empacota cada imagem de página na estrutura exata de dict que o vLLM espera (prompt + multi_modal_data.image), usando DeepseekOCR2Processor().tokenize_with_images(), que cuida de todo o pipeline de pré-processamento da imagem, incluindo dividir imagens grandes em 2–6 tiles via dynamic_preprocess() conforme a proporção, criar uma visão global 1024×1024 com padding, aplicar normalização de tensores e gerar a sequência completa de tokens com máscaras de imagem que o DeepseekOCR2ForCausalLM consome. 

Limpeza do OCR

O config.CROP_MODE do repositório controla se esse tileamento dinâmico é aplicado, o que melhora a precisão em layouts densos com textos pequenos. 

Após a geração, clean_ocr_output() remove tokens especiais de fim de sentença, elimina blocos de marcação de detecção (<|ref|>...<|/ref|><|det|>...<|/det|>), normaliza alguns operadores parecidos com LaTeX e reduz quebras de linha excessivas, produzindo texto mais limpo, que faz chunking melhor e recupera com mais confiabilidade no índice RAG a jusante.

Em seguida, vamos usar essa saída limpa para construir um índice RAG leve por página, de modo que as páginas se tornem pesquisáveis imediatamente após cada batch terminar.

Passo 4: engine de RAG

Agora que conseguimos extrair texto limpo de cada página, o próximo passo é torná-lo pesquisável imediatamente. Em vez de esperar o documento inteiro terminar o processamento, construímos um engine de Retrieval-Augmented Generation (RAG) leve por página que indexa o conteúdo de forma incremental. 

À medida que cada batch de páginas é processado, seu texto é convertido em embeddings e adicionado a um índice FAISS, permitindo que o usuário consulte o documento enquanto a indexação ainda está em andamento.

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

A classe acima define quatro componentes essenciais:

  • Indexação vetorial com FAISS: o engine usa faiss.IndexFlatIP para busca rápida por similaridade sobre embeddings normalizados. À medida que cada página é processada, seu conteúdo é imediatamente transformado em embedding e adicionado ao índice, permitindo recuperação em tempo real sem precisar reconstruir o índice.
  • Chunking por página: o método _split() quebra textos longos de página em chunks de ~1000 caracteres com base em limites de parágrafos. Isso melhora a correspondência semântica e evita embeddings de contextos muito longos/ruidosos, preservando o número da página para atribuição de fonte.
  • Atualizações com processamento em background: como OCR e indexação rodam em uma thread de fundo enquanto usuários podem consultar simultaneamente, um threading.Lock() protege atualizações e leituras do índice. 
  • Busca semântica rápida com lastro em fontes: o método query() gera o embedding da pergunta do usuário, recupera os top-k chunks similares e retorna o texto combinado com números de página e scores de similaridade. A propriedade indexed_pages acompanha o progresso para que a UI mostre quantas páginas já estão pesquisáveis.

Passo 5: engine vLLM

Neste passo, inicializamos o engine de inferência do DeepSeek OCR-2 usando vLLM. Este é o componente central que realiza a geração multimodal, recebendo as imagens tokenizadas das páginas e produzindo o texto de OCR estruturado. A configuração segue o setup de referência oficial do DeepSeek OCR-2 com vLLM, mas a encapsulamos em um carregador preguiçoso load_ocr_engine() para que o modelo só carregue quando o processamento começar, reduzindo o tempo de inicialização e evitando alocação desnecessária de GPU.

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')

Este trecho define os seguintes componentes centrais:

  • Registro do modelo: o ModelRegistry.register_model() conecta a classe customizada DeepseekOCR2ForCausalLM do repositório ao vLLM para que ele consiga lidar com entradas de imagem-token produzidas pelo DeepseekOCR2Processor.
  • Configuração do engine vLLM: os parâmetros do LLM() correspondem às definições de referência do repositório, incluindo block_size=256, max_model_len=8192, gpu_memory_utilization=0.9 e disable_mm_preprocessor_cache=True. Essas configurações são ajustadas para uma inferência de OCR estável e de alta vazão, mantendo o uso da memória de GPU previsível.
  • Decodificação controlada: o SamplingParams é idêntico à configuração original, isto é, decodificação determinística (temperature=0.0), limites amplos de saída e um NoRepeatNGramLogitsProcessor para evitar padrões repetitivos de texto — um problema comum em geração de OCR de longo comprimento.
  • Carregamento preguiçoso: em vez de inicializar o modelo no import, o engine carrega apenas quando load_ocr_engine() é chamado pela primeira vez. As chamadas seguintes reutilizam a mesma instância, evitando re-inicializações de GPU e mantendo a UI responsiva.

Agora, vamos conectar esse engine a um loop de processamento em lotes que roda OCR página a página e envia os resultados ao índice RAG para consultas em tempo real.

Passo 6: pipeline de “pergunte enquanto indexa”

Este passo é o coração do app. Implementamos um worker em background que recebe um PDF enviado e o transforma progressivamente em um índice pesquisável, em vez de esperar o documento inteiro terminar o 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.')

O fluxo do nosso pipeline inclui renderização das páginas, pré-processamento das entradas, execução da inferência com vLLM em minibatches, limpeza da saída de OCR e, por fim, adição de cada página ao PageRAG com FAISS. Aqui vai uma análise detalhada de cada etapa: 

  • Gerenciamento de estado: as variáveis globais (total_pages, is_processing, processing_error, stop_requested) acompanham progresso e falhas para que a UI mostre status ao vivo, permita parar/resetar e evite execuções conflitantes.
  • Pré-processamento em paralelo: o ThreadPoolExecutor(max_workers=config.NUM_WORKERS) converte imagens de página em entradas multimodais prontas para o modelo em paralelo. Isso mantém a GPU ocupada depois, garantindo que as entradas estejam preparadas antes da inferência.
  • Inferência em minibatches: em vez de rodar ocr_llm.generate() de uma vez em todas as páginas (padrão do script oficial), o código calcula num_batches e processa páginas em blocos de BATCH_SIZE. Isso habilita indexação progressiva, permitindo que os usuários comecem a perguntar após as primeiras 5 páginas (BATCH_SIZE = 5) serem indexadas, em vez de esperar o documento inteiro.
  • Parada entre batches: o stop_requested é checado antes de etapas custosas e no início de cada batch. Isso torna o botão Parar previsível: ele interrompe entre os lotes, após o lote atual terminar, evitando estados meio gravados ou saídas parciais.

Neste ponto, temos um pipeline fim a fim que converte um PDF em um índice crescente de busca, permitindo que o usuário consulte o documento enquanto o processamento continua em background. Agora, vamos adicionar os callbacks da UI que conectam esse worker ao aplicativo Gradio.

Passo 7: callbacks do aplicativo

Neste estágio, o pipeline de OCR e indexação já roda em background, mas o app ainda precisa de uma camada de controle que conecte as ações do usuário à lógica de processamento. 

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)

Esta camada de controle assume cinco responsabilidades principais:

  • Iniciar o processamento de forma assíncrona: a função start_processing() valida a entrada, reseta o índice de RAG e inicia process_pdf_background() em uma thread daemon. 
  • Interrupção segura entre batches: a função stop_processing() define a flag stop_requested em vez de terminar a execução imediatamente. O worker em background checa essa flag entre batches, garantindo parada limpa sem corromper o índice.
  • Reset de estado: o reset_all() limpa o índice FAISS, os contadores de progresso e o estado de erro. Ele também evita resets acidentais enquanto o processamento está ativo, protegendo contra estados inconsistentes.
  • Relato de progresso em tempo real: usamos a função refresh_progress() para ler o número de páginas indexadas do PageRAG e gerar uma barra de progresso em HTML dinâmica. Ela também comunica estados do sistema, como carregando, processamento ativo, parando, concluído ou erro.
  • Perguntas e respostas incrementais com lastro em fonte: por fim, o answer_question() consulta o índice FAISS e retorna os chunks mais relevantes com números de página e scores. Se a indexação ainda estiver rodando, a resposta indica explicitamente quantas páginas já foram pesquisadas, reforçando o comportamento “pergunte enquanto indexa”.

No passo final, vamos conectar esses callbacks a uma interface Gradio para expor todo o fluxo por uma UI web simples.

Passo 8: UI no Gradio

Neste passo final, empacotamos tudo em um aplicativo simples no Gradio. A UI foi pensada para permitir enviar um PDF, iniciar o processamento e já começar a fazer perguntas antes da indexação terminar. 

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)

O aplicativo Gradio tem duas colunas. À esquerda ficam o widget de upload de PDF e os botões Processar/Parar/Resetar; à direita, uma barra de progresso em HTML ao vivo. Isso faz o app parecer responsivo mesmo com o OCR rodando. Alguns destaques do app:

  • Indicador de progresso por polling: o gr.Timer(2) chama refresh_progress() a cada dois segundos e atualiza o HTML da barra. Como a indexação acontece em batches, o usuário vê o progresso avançar em degraus (e as mensagens mudam conforme estados como carregando, parando, erro ou conclusão).
  • Eventos conectados aos callbacks: os handlers .click() dos botões conectam ações da UI diretamente às suas funções backend, como start_processing() para iniciar o worker em background, stop_processing() para pedir parada limpa, reset_all() para limpar o estado e answer_question() para rodar a recuperação sobre as páginas já indexadas.
  • Interação de Q&A: as perguntas podem ser enviadas clicando em Ask ou pressionando Enter. A resposta é renderizada em Markdown, o que funciona bem para texto, mas nem tanto para conteúdo com código, equações ou caracteres especiais.

Por fim, demo.launch() inicia o app no Colab, gera um link compartilhável, renderiza inline e habilita logs de debug para ajudar na investigação de problemas durante o desenvolvimento.

Demo final

Conclusão

Neste tutorial, construímos um pipeline completo de inteligência de documentos com foco em visão usando o DeepSeek OCR-2. Partindo de um PDF bruto, renderizamos páginas em imagens de alta qualidade, executamos OCR multimodal via vLLM, limpamos e estruturamos o texto extraído e o indexamos incrementalmente em um repositório de RAG baseado em FAISS. 

Ao processar páginas em minibatches, o aplicativo viabiliza a experiência “pergunte enquanto indexa”, em que o usuário já pode consultar o documento após o primeiro lote, sem esperar a conclusão total.

Porém, dois limites chamaram atenção nos testes: regiões com muitas fórmulas nem sempre são transcritas com precisão — um desafio conhecido em modelos de OCR visão–linguagem, mesmo com prompts de grounding. 

Além disso, algumas páginas longas exibem padrões de saída repetitivos. O pipeline oficial mitiga isso com um NoRepeatNGramLogitsProcessor (ngram_size=20), mas tabelas densas e layouts multicoluna ainda podem disparar loops de geração — nesses casos, a página é ignorada silenciosamente via flag SKIP_REPEAT.

Para evoluir, você pode experimentar adicionar uma camada leve de LLM (por exemplo, chamando a API da Anthropic ou da OpenAI) para resumir os trechos recuperados em vez de retornar apenas snippets brutos. 

Outras melhorias práticas incluem persistir o índice FAISS em disco para reutilização entre sessões, suportar múltiplos PDFs em um único índice, expor parâmetros ajustáveis como BATCH_SIZE e top_k diretamente na UI do Gradio ou adicionar chunking sensível à estrutura (títulos, tabelas e limites de seções) para melhorar a precisão da recuperação.

FAQs

Isso usa o pipeline oficial do DeepSeek OCR-2?

Sim. O backend espelha a abordagem do repositório DeepSeek-OCR2-vllm, incluindo o pré-processamento DeepseekOCR2Processor junto com o DeepseekOCR2ForCausalLM registrado no vLLM e a função LLM.generate().

Por que não usar `AutoModel.from_pretrained()`?

O caminho AutoModel.from_pretrained() tende a quebrar por incompatibilidades entre transformers / flash-attn / CUDA ABI e mudanças em APIs internas. O caminho via vLLM do repositório é mais estável para este fluxo.

Como melhorar a qualidade de tabelas?

Tente aumentar o dpi (de 144 para 168 ou 192) e mantenha o crop_mode consistente com a configuração do repositório. Observe que tabelas são frequentemente sensíveis à resolução.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Sou Especialista Google Developers em ML (Gen AI), tricampeã no Kaggle e Embaixadora Women Techmakers, com mais de três anos de experiência na área de tecnologia. Cofundei uma startup de saúde em 2020 e atualmente faço um mestrado em ciência da computação na Georgia Tech, com foco em aprendizado de máquina.

Tópicos
Inteligência Artificial
Modelos de idiomas grandes

Principais cursos da DataCamp

Curso

Trabalhando com DeepSeek em Python

3 h
1.3K
Descubra do que realmente se tratava todo o hype do DeepSeek! Crie aplicações usando os modelos R1 e V3 da DeepSeek.
Ver detalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow
Relacionado

Tutorial

DeepSeek-Coder-V2 Tutorial: Exemplos, instalação, padrões de referência

O DeepSeek-Coder-V2 é um modelo de linguagem de código de código aberto que rivaliza com o desempenho do GPT-4, Gemini 1.5 Pro, Claude 3 Opus, Llama 3 70B ou Codestral.
Dimitri Didmanidze's photo

Dimitri Didmanidze

8 min

Tutorial

Tutorial do DeepChecks: Automatizando os testes de machine learning

Saiba como realizar a validação de dados e modelos para garantir um desempenho robusto de machine learning usando nosso guia passo a passo para automatizar testes com o DeepChecks.
Abid Ali Awan's photo

Abid Ali Awan

12 min

Tutorial

Guia de Introdução ao Ajuste Fino de LLMs

O ajuste fino dos grandes modelos de linguagem (LLMs, Large Language Models) revolucionou o processamento de linguagem natural (PLN), oferecendo recursos sem precedentes em tarefas como tradução de idiomas, análise de sentimentos e geração de textos. Essa abordagem transformadora aproveita modelos pré-treinados como o GPT-2, aprimorando seu desempenho em domínios específicos pelo processo de ajuste fino.
Josep Ferrer's photo

Josep Ferrer

11 min

Tutorial

Como criar aplicativos LLM com o tutorial LangChain

Explore o potencial inexplorado dos modelos de linguagem grandes com o LangChain, uma estrutura Python de código aberto para criar aplicativos avançados de IA.
Moez Ali's photo

Moez Ali

12 min

Tutorial

Tutorial do Chroma DB: Um guia passo a passo

Com o Chroma DB, você pode gerenciar facilmente documentos de texto, converter texto em embeddings e fazer pesquisas de similaridade.
Abid Ali Awan's photo

Abid Ali Awan

10 min

Tutorial

Como treinar um LLM com o PyTorch

Domine o processo de treinamento de grandes modelos de linguagem usando o PyTorch, desde a configuração inicial até a implementação final.
Zoumana Keita 's photo

Zoumana Keita

8 min

Ver MaisVer Mais