Agent OCR revolutionar: deepseek-OCR + LLama4 + RAG

de | noiembrie 6, 2025

DeepSeek-OCR folosește o abordare diferită: mai întâi convertește textul în imagini, apoi utilizează tokenuri vizuale pentru a comprima și reprezenta aceste informații. Imaginează-ți că ai un articol de 10.000 de cuvinte — în loc ca AI-ul să îl citească cuvânt cu cuvânt, poate pur și simplu să „arunce o privire” la o imagine pentru a înțelege și reconstrui textul original.

Acesta nu este doar un instrument obișnuit de recunoaștere a textului — este o tehnologie nouă de compresie optică contextuală care folosește metode vizuale pentru a rezolva provocarea procesării textelor lungi, oferind o abordare complet nouă pentru gestionarea cantităților masive de informații din documente.

Oricine a folosit un model lingvistic mare (LLM) a întâmpinat o problemă comună:

Când îi ceri modelului să rezume zeci de mii de cuvinte din notițe de conferință sau lucrări academice, acesta începe să-și piardă memoria. Acest lucru se datorează faptului că complexitatea cvadratică a lungimii secvenței limitează inerent GPT, Gemini și Claude — cu cât intrarea este mai lungă, cu atât necesită mai multă putere de calcul.

Dar oamenii nu sunt așa. Putem arunca o privire la o notiță sau un diagramă și să ne amintim instantaneu un pasaj întreg.

În mod tradițional, pentru ca AI-ul să înțeleagă documente lungi, întregul document trebuie convertit în text digital. Acest proces consumă un număr mare de tokeni (unitățile folosite de AI pentru a procesa informația), rezultând într-o eficiență computațională scăzută.

DeepSeek-OCR folosește o abordare diferită: mai întâi convertește textul în imagini, apoi folosește tokenuri vizuale pentru a comprima și reprezenta aceste informații. Imaginează-ți că ai un articol de 10.000 de cuvinte — în loc să citiți AI-ul cuvânt cu cuvânt, poate pur și simplu să „arunce o privire” la o imagine pentru a înțelege și reconstrui textul original.

Descoperirea esențială constă în capacitatea sa de a reprezenta informații bogate într-o singură imagine care conține textul documentului, folosind mult mai puțini tokeni decât echivalentul textului. Aceasta înseamnă că compresia optică cu tokenuri vizuale poate atinge rapoarte de compresie mai mari, permițându-ne să facem mai mult cu mai puține resurse.

Iata o demonstrație rapidă a unui chatbot live pentru a-ți arăta ce vreau să spun.

Voi pune chatbot-ului o întrebare: „Care sunt principalele concluzii?” Dacă te uiți la modul în care chatbot-ul generează răspunsul, vei observa că agentul extrage textul de pe fiecare pagină, dar dacă o pagină conține mai puțin de 50 de caractere sau nu are text încorporat, o convertește într-o imagine de înaltă rezoluție și o trimite către DeepSeek-OCR pe Replicate, care folosește o abordare inovatoare numită „Compresie Optică Contextuală”, unde documentul este convertit în tokenuri vizuale și informația este comprimată — permițând practic AI-ului să „arunce o privire” la o reprezentare de imagine, în loc să citească cuvânt cu cuvânt, ceea ce poate transforma un articol de 10.000 de cuvinte într-un format comprimat mult mai eficient.

Odată ce tot textul este extras, sistemul îl împarte în segmente de 500 de caractere cu o suprapunere de 50 de caractere pentru a menține contextul, convertește fiecare segment în vectori matematici folosind embedding-urile OpenAI și îi stochează într-o bază de date vectorială Chroma care este stocata pe disc pentru utilizare viitoare.

Când pui o întrebare, agentul caută prin acești vectori pentru a găsi cele 5 segmente de document cele mai asemănătoare semantic, le asamblează într-un prompt de context împreună cu întrebarea ta și instrucțiunile de a cita numerele paginilor, apoi trimite totul către modelul Llama 3.1 405B, care rulează pe API-ul de streaming al Replicate, care procesează promptul și generează un răspuns inteligent segment cu segment, în timp real.

Apoi generează răspunsul și citațiile din documentul sursă, arătând de pe ce pagini provine informația, creând astfel un agent RAG complet capabil să înțeleagă orice PDF.

Ce face Deepseek-OCR unic ?

DeepSeek-OCR este un model complet de recunoaștere optică a caracterelor (OCR) și de parsare a documentelor, conceput pentru a realiza compresia contextului optic.

Acest model este alcătuit din două componente principale: un DeepEncoder care comprimă imaginea de înaltă rezoluție într-un număr mic de tokenuri vizuale și un decodor DeepSeek-3B-MoE (un model lingvistic Mixture-of-Experts) care restaurează textul original din secvența de tokenuri vizuale.

DeepEncoder (aproximativ 380 de milioane de parametri) include un mecanism de atenție pe ferestre bazat pe SAM pentru extragerea caracteristicilor locale ale imaginii și, prin inserarea unui CNN cu două straturi și compresie de 16x între acestea, comprimă semnificativ o imagine de 1024×1024 pixeli de la 4096 de patch-uri la aproximativ 256 de tokenuri.

Partea de decodor care primește aceste tokenuri vizuale are în total 3 miliarde de parametri (aproximativ 570 de milioane fiind efectivi în timpul inferenței) și are o structură MoE care folosește dinamic 6 experți pe pas dintr-un total de 64, permițând o reconstrucție a textului ușoară, dar eficientă.

Cu această arhitectură, DeepSeek-OCR adoptă o abordare neconvențională, convertind conținutul unui document text într-o „imagine” și apoi citindu-l.

Text Tokens vs. Visual Tokens: Diferenta fundamentala

În modelele lingvistice tradiționale (LLM), textul este împărțit în tokenuri de text (de obicei cuvinte sau subcuvinte). Fiecărui token i se atribuie un ID fix în vocabular și este mapat într-un vector printr-un „tabel de căutare” (stratul încorporat). Deși acest proces este eficient, puterea sa expresivă este limitată de vocabularul restrâns.

Tokenurile vizuale sunt complet diferite. În loc să provină dintr-un tabel fix de căutare, ele sunt vectori continui generați direct din pixeli de imagine de către o rețea neurală (encoder vizual). Aceasta înseamnă:

  • Densitate mai mare a informației: Tokenurile vizuale există într-un spațiu vectorial continuu și pot codifica informații mai bogate și mai nuanțate decât tokenurile discrete de text. Un token vizual poate reprezenta culoarea, forma, textura și relațiile spațiale dintr-o zonă, nu doar un cuvânt sau un subcuvânt.
  • Percepție globală a pattern-urilor: Encoderul vizual poate captura informații globale, cum ar fi aranjamentul general, tipărirea și stilul fontului textului, care se pierd în secvența simplă de tokenuri de text.
  • Spațiu mai larg de exprimare: În teorie, „vocabularul” tokenurilor vizuale este infinit, deoarece acestea sunt vectori continui generați direct din pixeli, nu selectați dintr-un dicționar fix.

Codul

Iata si codul cu care am testat. (in Google Colab)

%pip install PyMuPDF
%pip install langchain-chroma
%pip install langchain-openai
%pip install replicate

import os
import replicate
from langchain_openai import OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from langchain_core.language_models.llms import LLM
from typing import List, Optional, Any
import fitz
from pathlib import Path
from dotenv import load_dotenv
from google.colab import userdata

# Load environment variables from .env file, including REPLICATE_API_TOKEN if present
load_dotenv()

# Ensure REPLICATE_API_TOKEN is set from Colab secrets if not in .env
if "REPLICATE_API_TOKEN" not in os.environ:
  try:
    os.environ["REPLICATE_API_TOKEN"] = userdata.get('REPLICATE_API_TOKEN')
  except:
    print("Seteaza REPLICATE_API_TOKEN in Colab secrets.")

class Llama(LLM):
    model: str = "meta/meta-llama-3.1-405b-instruct"
    max_tokens: int = 1024
    temperature: float = 0.7

    @property
    def _llm_type(self) -> str:
        return "replicate_llama"

    def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        input_data = {
            "prompt": prompt,
            "max_tokens": self.max_tokens,
            "temperature": self.temperature
        }

        output = ""
        for event in replicate.stream(self.model, input=input_data):
            output += str(event)

        return output

class OCRPDFLoader:
    def __init__(self, file_path: str, use_ocr: bool = False, text_threshold: int = 50, replicate_api_token: Optional[str] = None):
        self.file_path = file_path
        self.use_ocr = use_ocr
        self.text_threshold = text_threshold
        self.replicate_api_token = replicate_api_token

    def load(self) -> List[Document]:
        doc = fitz.open(self.file_path)
        documents = []

        for page_num in range(len(doc)): # aici poti sa limitezi numarul de pagini
            page = doc[page_num]
            text = page.get_text()

            if self.use_ocr or len(text.strip()) < self.text_threshold:
                print(f"OCR: pagina {page_num + 1}")
                text = self._ocr_page(page, page_num)

            if text.strip():
                documents.append(Document(
                    page_content=text.strip(),
                    metadata={
                        'source': self.file_path,
                        'page': page_num + 1,
                        'filename': Path(self.file_path).name
                    }
                ))

        doc.close()
        return documents

    def _ocr_page(self, page, page_num, temp_dir='./temp_ocr'):
        os.makedirs(temp_dir, exist_ok=True)

        pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
        img_path = f"{temp_dir}/page_{page_num}.png"
        pix.save(img_path)

        with open(img_path, "rb") as image_file:
            input_data = {
                "image": image_file,
                "task_type": "Free OCR"
            }

            # Initialize Replicate client with the token
            client = replicate.Client(api_token=self.replicate_api_token)

            output = client.run(
                "lucataco/deepseek-ocr:cb3b474fbfc56b1664c8c7841550bccecbe7b74c30e45ce938ffca1180b4dff5",
                input=input_data
            )

        os.remove(img_path)
        return output

class LangChainPDFRAG:
    def __init__(self,
                 llm_model='meta/meta-llama-3.1-405b-instruct',
                 embedding_model='text-embedding-3-small',
                 persist_directory='./chroma_db'):

        self.llm = Llama(model=llm_model)
        self.embeddings = OpenAIEmbeddings(model=embedding_model)
        self.persist_directory = persist_directory
        self.vectorstore = None

        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=50,
            separators=["\n\n", "\n", ". ", " ", ""]
        )

        if os.path.exists(persist_directory):
            self.vectorstore = Chroma(
                persist_directory=persist_directory,
                embedding_function=self.embeddings
            )

    def add_pdf(self, pdf_path: str, use_ocr: bool = False, replicate_api_token: Optional[str] = None): 
        loader = OCRPDFLoader(pdf_path, use_ocr=use_ocr, replicate_api_token=replicate_api_token) # API Replicate token
        documents = loader.load()
        splits = self.text_splitter.split_documents(documents)

        if self.vectorstore is None:
            self.vectorstore = Chroma.from_documents(
                documents=splits,
                embedding=self.embeddings,
                persist_directory=self.persist_directory
            )
        else:
            self.vectorstore.add_documents(splits)

        print(f"Adaugat {len(splits)} chunks din {Path(pdf_path).name}")
        return len(splits)

    def query(self, question: str):
        if self.vectorstore is None:
            raise ValueError("No documents.")

        retriever = self.vectorstore.as_retriever(search_kwargs={"k": 5})

        def format_docs(docs):
            return "\n\n".join([doc.page_content pentru doc in docs])

        prompt = ChatPromptTemplate.from_template(
            "You are a helpful assistant. Answer based on the context provided. Cite page numbers when relevant.\n\n"
            "Context:\n{context}\n\n"
            "Question: {question}\n\n"
            "Answer:"
        )

        chain = (
            {"context": retriever | format_docs, "question": RunnablePassthrough()}
            | prompt
            | self.llm
            | StrOutputParser()
        )

        docs = retriever.invoke(question)
        answer = chain.invoke(question)

        return {
            'answer': answer,
            'sources': [
                {
                    'filename': doc.metadata.get('filename'),
                    'page': doc.metadata.get('page'),
                    'content': doc.page_content[:200]
                }
                for doc in docs
            ]
        }

if __name__ == "__main__":
    # Load the OpenAI API key from environment variables first
    os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')

    # Using Llama 3.1 405B from Replicate
    rag = LangChainPDFRAG(llm_model='meta/meta-llama-3.1-405b-instruct')

    # Get REPLICATE_API_TOKEN from environment
    replicate_api_token = os.environ.get("REPLICATE_API_TOKEN")

    if not replicate_api_token:
        print("REPLICATE_API_TOKEN nu exista. Please set it in Colab secrets.")
    else:
        rag.add_pdf('fisier.pdf', use_ocr=False, replicate_api_token=replicate_api_token) # Pass the token here

        result = rag.query('Care sunt principalele concluzii?')

        print("=== Raspuns ===")
        print(result['answer'])

        print("\n=== Surse ===")
        for source in result['sources']:
            print(f"- {source['filename']}, Page {source['page']}")