LLM Guides

Extract Text from Images in Python with LLMAPI

Aug 12, 2026

There is a special kind of product pain that starts with a user uploading a picture of a document.

Maybe it is a receipt. Maybe it is a scanned form. Maybe it is a PDF that looks like a document but behaves like a flat image wearing a disguise. Maybe it is a photo of a whiteboard taken at an angle so dramatic it deserves cinematography credit.

And then the app is expected to “just read it.”

That is the job here: teach a Python app to extract text from images, scans, and PDFs so users do not have to copy text by hand like it is 2006.

We’ll build a practical OCR workflow with Python, show local and cloud OCR options, clean the extracted text, handle PDFs, and use LLMAPI to turn raw OCR output into structured fields, summaries, searchable text, and cleaner document data.

The OCR mess we are solving

Text extraction sounds simple until the file arrives.

A clean screenshot is easy. A crumpled receipt is not. A scanned PDF with rotated pages is a different beast. A form with tables, checkboxes, handwriting, and stamps starts looking like a tiny paperwork swamp.

Common inputs:

Input typeWhat can go wrong
ScreenshotUsually clean, but may have tiny text
Phone photoSkew, blur, shadows, glare
Scanned documentRotation, low contrast, compression
ReceiptNarrow layout, faded ink, totals mixed with items
FormTables, boxes, labels, handwriting
PDFCould contain real text, scanned pages, or both
ID/document scanSensitive data, strict privacy needs
Whiteboard photoPerspective distortion, messy handwriting
InvoiceTables, totals, taxes, line items
Multi-page PDFPage ordering, headers, footers, repeated text

So the pipeline should not be “send image, hope.”

A better OCR workflow looks like this:

file upload
→ detect file type
→ extract embedded text if available
→ OCR image/scanned pages
→ clean raw text
→ preserve page/line metadata
→ use LLMAPI for structure or summary
→ validate output

This gives the app options instead of treating every file like the same JPEG.

What “extract text” can mean

Before coding, decide what the app needs.

Sometimes we only need plain text.

Example:

Payment due within 30 days. Please reference invoice INV-1042.

Sometimes we need structured data:

{
  "invoice_number": "INV-1042",
  "payment_terms": "Payment due within 30 days"
}

Sometimes we need layout:

{
  "page": 1,
  "lines": [
    {
      "text": "Invoice INV-1042",
      "box": {
        "x": 0.12,
        "y": 0.08,
        "width": 0.30,
        "height": 0.04
      }
    }
  ]
}

Different output goals need different tools.

GoalBest approach
Plain text from imageTesseract, PaddleOCR, Google Vision
Text from scanned PDFConvert pages to images, then OCR
Forms and tablesAzure Document Intelligence, Amazon Textract, Google Document AI
Receipts/invoicesOCR + LLMAPI or document-specific parser
Searchable archiveOCR text + page metadata + embeddings
Structured fieldsOCR first, LLMAPI cleanup/extraction second
SummariesOCR first, LLMAPI summary second
Sensitive documentsLocal OCR or strict vendor/privacy review

That distinction saves us from overbuilding.

A screenshot reader and an invoice parser are cousins, not twins.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, OCR workflows, document parsing, Python automation, structured extraction, LLM post-processing, and developer tutorials. We also checked current documentation from LLMAPI, Tesseract, pytesseract, Google Cloud Vision, Azure Document Intelligence, Amazon Textract, and PaddleOCR while preparing this guide.

The main lesson is that OCR and LLMs solve different parts of the problem.

Tesseract is an open-source OCR engine available under the Apache 2.0 license, and its documentation includes image quality guidance such as thresholding, preprocessing, and handling noisy scans. Google Cloud Vision supports OCR text detection for images, while its feature types include DOCUMENT_TEXT_DETECTION for dense document OCR. Azure Document Intelligence’s Read OCR model extracts printed and handwritten text from PDFs and scanned images as lines and words, while Amazon Textract can detect lines and words in document images.

LLMAPI fits after OCR: it can clean, structure, summarize, classify, or validate extracted text using an OpenAI-compatible chat completions pattern.

The pipeline blocks

We’ll build the system as blocks.

BlockJob
IntakeAccept image or PDF
PrecheckValidate file type, size, and page count
Text layerExtract embedded PDF text when possible
OCR layerRead text from images/scans
Cleanup layerNormalize spacing, line breaks, weird characters
Structure layerUse LLMAPI to extract fields or summaries
Validation layerCheck required fields and confidence
Storage layerSave raw OCR, cleaned text, and structured output

This keeps the system flexible.

If Tesseract is enough, use it.
If a document is complex, switch to Google Vision, Azure Document Intelligence, Amazon Textract, or PaddleOCR.
If the text needs business meaning, send the OCR result to LLMAPI.

Step 1: Set up the Python project

Create the project:

mkdir python-image-text-extraction
cd python-image-text-extraction
python -m venv .venv

Activate it.

macOS/Linux:

source .venv/bin/activate

Windows PowerShell:

.venv\Scripts\Activate.ps1

Install the core packages:

pip install pillow pytesseract opencv-python python-dotenv openai pydantic fastapi uvicorn pymupdf

You also need the Tesseract OCR engine installed on your machine. The pytesseract package is a Python wrapper for Tesseract and can call functions like image_to_string() on images.

For macOS:

brew install tesseract

For Ubuntu/Debian:

sudo apt-get update
sudo apt-get install tesseract-ocr

For Windows, install Tesseract separately and configure the executable path if needed.

Step 2: Extract text from a single image

Create ocr_image.py:

from PIL import Image
import pytesseract


def extract_text_from_image(image_path: str) -> str:
    image = Image.open(image_path)
    text = pytesseract.image_to_string(image)
    return text.strip()


if __name__ == "__main__":
    result = extract_text_from_image("sample_receipt.jpg")
    print(result)

Run it:

python ocr_image.py

This is the smallest useful OCR script.

It works best on clean images with clear printed text.

Step 3: Add image preprocessing

OCR quality often improves when we clean the image first.

Common preprocessing:

StepWhy
GrayscaleSimplifies image data
ResizeHelps tiny text
ThresholdingImproves contrast
DenoisingReduces speckles
DeskewingFixes tilted scans
CroppingRemoves irrelevant background
Rotation correctionHandles sideways pages

Create preprocess.py:

import cv2


def preprocess_for_ocr(image_path: str, output_path: str = "preprocessed.png") -> str:
    image = cv2.imread(image_path)

    if image is None:
        raise ValueError(f"Could not read image: {image_path}")

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    scaled = cv2.resize(
        gray,
        None,
        fx=2,
        fy=2,
        interpolation=cv2.INTER_CUBIC
    )

    thresholded = cv2.threshold(
        scaled,
        0,
        255,
        cv2.THRESH_BINARY + cv2.THRESH_OTSU
    )[1]

    cv2.imwrite(output_path, thresholded)

    return output_path

Use it:

from preprocess import preprocess_for_ocr
from ocr_image import extract_text_from_image

clean_image = preprocess_for_ocr("sample_receipt.jpg")
text = extract_text_from_image(clean_image)

print(text)

Tesseract’s quality guidance notes that it performs internal image processing through Leptonica, but also points to preprocessing techniques such as thresholding when quality needs improvement.

Preprocessing will not save every terrible photo.

But it helps enough to be worth adding.

Step 4: Extract text with boxes

For many apps, plain text is not enough. We may want line or word positions.

Create ocr_with_boxes.py:

from PIL import Image
import pytesseract


def extract_words_with_boxes(image_path: str) -> list[dict]:
    image = Image.open(image_path)
    data = pytesseract.image_to_data(
        image,
        output_type=pytesseract.Output.DICT
    )

    words = []

    for index, text in enumerate(data["text"]):
        clean_text = text.strip()

        if not clean_text:
            continue

        confidence = float(data["conf"][index])

        if confidence < 0:
            confidence = None

        words.append({
            "text": clean_text,
            "confidence": confidence,
            "box": {
                "x": data["left"][index],
                "y": data["top"][index],
                "width": data["width"][index],
                "height": data["height"][index]
            }
        })

    return words

This gives us word-level layout.

Useful for:

  • Highlighting OCR results
  • Building review tools
  • Reconstructing tables
  • Locating fields
  • Showing users where a value came from
  • Debugging bad OCR

Step 5: Extract embedded text from PDFs first

Some PDFs already contain selectable text.

OCRing those pages is slower and less accurate than reading the text layer.

Use PyMuPDF:

import fitz


def extract_embedded_pdf_text(pdf_path: str) -> list[dict]:
    document = fitz.open(pdf_path)
    pages = []

    for page_index, page in enumerate(document):
        text = page.get_text("text").strip()

        pages.append({
            "page": page_index + 1,
            "text": text
        })

    return pages

Then check whether the text is usable:

def has_enough_text(pages: list[dict], min_chars: int = 50) -> bool:
    total_chars = sum(len(page["text"]) for page in pages)
    return total_chars >= min_chars

If the PDF has enough embedded text, use it.

If it does not, convert pages to images and OCR them.

Step 6: OCR scanned PDF pages

Create ocr_pdf.py:

from pathlib import Path
import fitz
from PIL import Image
import pytesseract


def render_pdf_page_to_image(page, zoom: float = 2.0) -> Image.Image:
    matrix = fitz.Matrix(zoom, zoom)
    pixmap = page.get_pixmap(matrix=matrix)

    mode = "RGB" if pixmap.alpha == 0 else "RGBA"

    return Image.frombytes(
        mode,
        [pixmap.width, pixmap.height],
        pixmap.samples
    )


def ocr_scanned_pdf(pdf_path: str) -> list[dict]:
    document = fitz.open(pdf_path)
    results = []

    for page_index, page in enumerate(document):
        image = render_pdf_page_to_image(page)
        text = pytesseract.image_to_string(image).strip()

        results.append({
            "page": page_index + 1,
            "text": text
        })

    return results

Now create one function that handles both PDF types:

from pathlib import Path
from ocr_pdf import ocr_scanned_pdf
from pdf_text import extract_embedded_pdf_text, has_enough_text


def extract_text_from_pdf(pdf_path: str) -> list[dict]:
    embedded_pages = extract_embedded_pdf_text(pdf_path)

    if has_enough_text(embedded_pages):
        return embedded_pages

    return ocr_scanned_pdf(pdf_path)

This is a big product win.

Real PDFs are messy. Some are digital, some are scanned, some are mixed. Your app should not make the user know the difference.

Step 7: Build one file router

Now we need one function for images and PDFs.

Create extract_text.py:

from pathlib import Path

from ocr_image import extract_text_from_image
from pdf_router import extract_text_from_pdf


IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".tiff", ".tif"}
PDF_EXTENSIONS = {".pdf"}


def extract_text_from_file(file_path: str) -> dict:
    path = Path(file_path)
    suffix = path.suffix.lower()

    if suffix in IMAGE_EXTENSIONS:
        text = extract_text_from_image(file_path)

        return {
            "file_type": "image",
            "pages": [
                {
                    "page": 1,
                    "text": text
                }
            ],
            "text": text
        }

    if suffix in PDF_EXTENSIONS:
        pages = extract_text_from_pdf(file_path)
        full_text = "\n\n".join(page["text"] for page in pages)

        return {
            "file_type": "pdf",
            "pages": pages,
            "text": full_text
        }

    raise ValueError(f"Unsupported file type: {suffix}")

Now the app can accept both images and PDFs without making the user choose a mode.

Step 8: Clean raw OCR text

OCR text is often ugly.

It may contain:

  • Broken line breaks
  • Extra spaces
  • Misread characters
  • Page headers repeated everywhere
  • Footer junk
  • Strange punctuation
  • Words split across lines
  • Random symbols
  • Table columns smashed together

Create clean_text.py:

import re


def clean_ocr_text(text: str) -> str:
    text = text.replace("\x0c", "\n")
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    text = re.sub(r" *\n *", "\n", text)

    # Fix common OCR spacing around punctuation.
    text = re.sub(r"\s+([,.;:!?])", r"\1", text)

    return text.strip()

Use it:

from extract_text import extract_text_from_file
from clean_text import clean_ocr_text

result = extract_text_from_file("scan.pdf")
cleaned = clean_ocr_text(result["text"])

print(cleaned)

Keep the raw OCR text too.

Raw text is useful for debugging and audit trails.

Step 9: Use LLMAPI to structure the extracted text

OCR gives us text.

LLMAPI can turn that text into structured data.

Create .env:

LLMAPI_API_KEY=your_llmapi_key_here
LLMAPI_BASE_URL=https://api.llmapi.ai/v1

Create llmapi_client.py:

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.environ["LLMAPI_API_KEY"],
    base_url=os.environ.get("LLMAPI_BASE_URL", "https://api.llmapi.ai/v1")
)

Create structure_text.py:

import json
from llmapi_client import client


def extract_document_fields(ocr_text: str, document_type: str = "generic") -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
You extract structured data from OCR text.

Return only valid JSON:
{
  "document_type": "string",
  "summary": "string",
  "fields": {
    "key": "value"
  },
  "tables": [],
  "warnings": ["string"]
}

Rules:
- Use only information present in the OCR text.
- Do not invent missing values.
- Use null when a field is not found.
- Add warnings for unclear, corrupted, or low-confidence text.
- Preserve important numbers exactly as written when possible.
"""
            },
            {
                "role": "user",
                "content": json.dumps({
                    "document_type_hint": document_type,
                    "ocr_text": ocr_text
                })
            }
        ],
        temperature=0
    )

    return json.loads(response.choices[0].message.content)

Example:

from extract_text import extract_text_from_file
from clean_text import clean_ocr_text
from structure_text import extract_document_fields

result = extract_text_from_file("invoice_scan.pdf")
cleaned = clean_ocr_text(result["text"])

structured = extract_document_fields(
    cleaned,
    document_type="invoice"
)

print(json.dumps(structured, indent=2))

This is where the app becomes useful.

OCR reads. LLMAPI organizes.

Step 10: Use document-specific schemas

Generic extraction is fine for demos.

Real products should define schemas.

For an invoice:

{
  "invoice_number": "string or null",
  "vendor_name": "string or null",
  "invoice_date": "string or null",
  "due_date": "string or null",
  "subtotal": "string or null",
  "tax": "string or null",
  "total": "string or null",
  "currency": "string or null",
  "line_items": [
    {
      "description": "string",
      "quantity": "string or null",
      "unit_price": "string or null",
      "amount": "string or null"
    }
  ],
  "warnings": ["string"]
}

Create invoice_extractor.py:

import json
from llmapi_client import client


def extract_invoice_fields(ocr_text: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": """
Extract invoice data from OCR text.

Return only valid JSON:
{
  "invoice_number": null,
  "vendor_name": null,
  "invoice_date": null,
  "due_date": null,
  "subtotal": null,
  "tax": null,
  "total": null,
  "currency": null,
  "line_items": [
    {
      "description": "string",
      "quantity": null,
      "unit_price": null,
      "amount": null
    }
  ],
  "warnings": []
}

Rules:
- Use only OCR-supported values.
- Do not calculate totals unless the OCR text clearly provides the numbers.
- Preserve dates and amounts as written.
- Add warnings for unreadable or ambiguous values.
"""
            },
            {
                "role": "user",
                "content": ocr_text
            }
        ],
        temperature=0
    )

    return json.loads(response.choices[0].message.content)

Now the output is more predictable.

LLMAPI is especially useful when OCR text is readable but poorly structured.

Step 11: Validate the structured output

Do not let model output go straight into a database.

Use Pydantic.

Create schemas.py:

from typing import Optional, List
from pydantic import BaseModel, Field


class LineItem(BaseModel):
    description: str
    quantity: Optional[str] = None
    unit_price: Optional[str] = None
    amount: Optional[str] = None


class InvoiceData(BaseModel):
    invoice_number: Optional[str] = None
    vendor_name: Optional[str] = None
    invoice_date: Optional[str] = None
    due_date: Optional[str] = None
    subtotal: Optional[str] = None
    tax: Optional[str] = None
    total: Optional[str] = None
    currency: Optional[str] = None
    line_items: List[LineItem] = Field(default_factory=list)
    warnings: List[str] = Field(default_factory=list)

Validate:

from schemas import InvoiceData
from invoice_extractor import extract_invoice_fields

raw_invoice = extract_invoice_fields(cleaned_text)
invoice = InvoiceData.model_validate(raw_invoice)

print(invoice.model_dump_json(indent=2))

Validation catches broken JSON shapes, missing expected structures, and type mismatches.

It will not guarantee the OCR was correct, but it keeps your app from swallowing chaos whole.

Step 12: Add a FastAPI endpoint

Create app.py:

import shutil
from pathlib import Path
from uuid import uuid4

from fastapi import FastAPI, UploadFile, File, HTTPException

from extract_text import extract_text_from_file
from clean_text import clean_ocr_text
from structure_text import extract_document_fields

app = FastAPI(
    title="Image Text Extraction API",
    description="Extract text from images, scans, and PDFs with Python and LLMAPI.",
    version="1.0.0"
)

UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

ALLOWED_EXTENSIONS = {
    ".png",
    ".jpg",
    ".jpeg",
    ".webp",
    ".tiff",
    ".tif",
    ".pdf"
}


@app.get("/health")
def health_check():
    return {
        "status": "ok"
    }


@app.post("/extract-text")
def extract_text_endpoint(file: UploadFile = File(...)):
    suffix = Path(file.filename).suffix.lower()

    if suffix not in ALLOWED_EXTENSIONS:
        raise HTTPException(
            status_code=400,
            detail="Unsupported file type."
        )

    file_id = str(uuid4())
    file_path = UPLOAD_DIR / f"{file_id}{suffix}"

    with file_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    extracted = extract_text_from_file(str(file_path))
    cleaned_text = clean_ocr_text(extracted["text"])

    structured = extract_document_fields(cleaned_text)

    return {
        "file_id": file_id,
        "file_type": extracted["file_type"],
        "pages": extracted["pages"],
        "cleaned_text": cleaned_text,
        "structured": structured
    }

Run it:

uvicorn app:app --reload

Test:

curl -X POST "http://127.0.0.1:8000/extract-text" \
  -F "[email protected]"

For production, add background jobs for large PDFs.

A 70-page scanned PDF should not be processed inside a normal request while the user stares into the void.

Step 13: Add Google Cloud Vision as a stronger OCR option

Tesseract is great for local OCR and simple workflows.

Cloud OCR may perform better for complex images, dense text, or multi-language documents.

Google Cloud Vision supports text detection from images and document text detection for dense text.

Install:

pip install google-cloud-vision

Example:

from google.cloud import vision


def extract_text_with_google_vision(image_path: str) -> str:
    client = vision.ImageAnnotatorClient()

    with open(image_path, "rb") as image_file:
        content = image_file.read()

    image = vision.Image(content=content)

    response = client.document_text_detection(image=image)

    if response.error.message:
        raise RuntimeError(response.error.message)

    return response.full_text_annotation.text.strip()

Use cloud OCR when:

  • Local OCR quality is weak
  • You need better dense-document OCR
  • You want managed scaling
  • You already use Google Cloud
  • You process many image formats
  • You need stronger handwriting/multi-language support

Keep vendor privacy and storage terms in mind.

Step 14: Add Azure Document Intelligence for document-heavy OCR

Azure Document Intelligence is useful when the app processes PDFs, scans, forms, and document layouts.

The Read OCR model extracts printed and handwritten text from PDFs and scanned images as lines and words. Azure’s model overview also describes document models that can extract text, tables, paragraphs, key-value pairs, fields, selection marks, and layout depending on the model.

Use Azure Document Intelligence when:

  • PDFs and scans are common
  • You need lines, words, layout, and language data
  • You need form/table structure
  • You already use Azure
  • You want document-processing models beyond plain OCR
  • You need higher-quality OCR for scanned documents

For invoice/form-heavy products, document intelligence APIs may save a lot of custom parsing work.

Step 15: Add Amazon Textract for forms and documents

Amazon Textract is another strong option for AWS-based apps.

Amazon Textract can detect lines of text and words in document images, and AWS also describes synchronous and asynchronous operations for document text detection. AWS’s Textract materials also describe extracting printed text, handwriting, structured data, and tables from images and scans of documents.

Use Textract when:

  • Your files are already in S3
  • You process scanned documents
  • You need forms and tables
  • You want async document jobs
  • You need AWS-native permissions and storage
  • You are building invoice, receipt, or document workflows

A common architecture:

S3 upload
→ Textract OCR/document analysis
→ normalized text and fields
→ LLMAPI cleanup or summary
→ database/review queue

This is a strong pattern for document-heavy products.

Step 16: Add PaddleOCR for local or custom OCR

PaddleOCR is useful when you want a more modern open-source OCR toolkit with Python support.

PaddleOCR docs include Python usage and pipelines for OCR, document parsing, layout/table-related workflows, and models such as PP-OCR versions.

Install:

pip install "paddleocr[all]"

Example shape:

from paddleocr import PaddleOCR


ocr = PaddleOCR(use_angle_cls=True, lang="en")


def extract_text_with_paddleocr(image_path: str) -> str:
    results = ocr.ocr(image_path)
    lines = []

    for page in results:
        for line in page:
            text = line[1][0]
            lines.append(text)

    return "\n".join(lines)

Use PaddleOCR when:

  • You want local OCR
  • You need stronger OCR than basic Tesseract for some images
  • You process multilingual text
  • You want open-source control
  • You need layout/table/document parsing options
  • You can manage dependencies

As always, test it on your own documents.

OCR quality depends heavily on the actual files.

Step 17: Choose the right OCR route

Here is the practical chooser.

SituationGood starting point
Clean screenshotTesseract or Google Vision
Simple scanned imageTesseract with preprocessing
Messy phone photoGoogle Vision or PaddleOCR
Multi-page scanned PDFAzure Document Intelligence, Textract, or PDF page OCR
Forms and tablesAzure Document Intelligence or Textract
Receipt/invoice extractionOCR + LLMAPI schema, or specialized parser
Sensitive local filesTesseract or PaddleOCR locally
High-volume productionCloud OCR with queues
Need structured fieldsOCR + LLMAPI + validation
Need searchable archiveOCR + page metadata + embeddings

Start simple.

Then upgrade only where the document mess demands it.

Step 18: Add OCR quality checks

OCR can fail quietly.

Add quality warnings.

Create quality.py:

def check_ocr_quality(text: str, page_count: int = 1) -> list[str]:
    warnings = []

    clean = text.strip()

    if len(clean) < 20:
        warnings.append("Very little text was extracted.")

    if page_count > 1 and len(clean) / page_count < 50:
        warnings.append("Average extracted text per page is low.")

    replacement_ratio = clean.count("�") / max(len(clean), 1)

    if replacement_ratio > 0.01:
        warnings.append("Text contains many replacement characters.")

    weird_symbol_count = sum(
        1 for char in clean
        if not char.isalnum() and not char.isspace() and char not in ".,;:!?$€£¥%-()/@#&+"
    )

    if weird_symbol_count > len(clean) * 0.05:
        warnings.append("Text contains many unusual symbols.")

    return warnings

Use it:

warnings = check_ocr_quality(
    cleaned_text,
    page_count=len(extracted["pages"])
)

Send warnings to LLMAPI too.

That lets the structured extractor avoid overconfidence.

Step 19: Add review flags

Some OCR results should go to review.

Examples:

  • No text found
  • Very low text length
  • Required fields missing
  • Total amount unreadable
  • Date ambiguous
  • Multiple invoice numbers found
  • Low confidence on important words
  • Table line items look corrupted
  • ID/document data present
  • Signature or handwriting detected

Review flag example:

{
  "review_required": true,
  "reasons": [
    "Invoice total was not found.",
    "OCR quality warning: text contains many unusual symbols."
  ]
}

Review is not failure.

It is how document systems avoid sending bad extracted data downstream.

Step 20: Store raw, cleaned, and structured outputs

Save three layers.

LayerWhy
Raw OCRDebug provider/model behavior
Cleaned textSearch and post-processing
Structured JSONApp workflows and database
Page metadataReview UI and source traceability
WarningsQuality and manual review
Model/provider metadataAudit and future debugging

Example record:

{
  "file_id": "file_123",
  "source_filename": "invoice_scan.pdf",
  "ocr_provider": "tesseract",
  "ocr_model": "local",
  "llm_model": "gpt-4o-mini",
  "pages": 3,
  "raw_text_path": "raw/file_123.txt",
  "cleaned_text_path": "clean/file_123.txt",
  "structured_json_path": "structured/file_123.json",
  "review_required": true,
  "created_at": "2026-08-24T15:04:00-05:00"
}

This helps when a user says:

“Why did your app read the total as $88.10?”

You can inspect the raw OCR, the cleaned text, and the structured extraction.

Privacy and security basics

Images, scans, and PDFs often contain sensitive information.

That can include:

  • Names
  • Addresses
  • Emails
  • Phone numbers
  • Signatures
  • Financial data
  • Tax IDs
  • Health details
  • Legal information
  • IDs and passports
  • Employee records
  • Customer data

Basic rules:

  • Keep API keys on the backend.
  • Validate file types and file size.
  • Use encrypted storage.
  • Restrict access to uploaded files.
  • Avoid logging raw OCR text.
  • Redact sensitive data where possible.
  • Define retention periods.
  • Let users delete files where required.
  • Review vendor data handling before using cloud OCR.
  • Use local OCR for highly sensitive workflows when needed.
  • Add audit logs for document access.

A safe product copy line:

We extract text from your uploaded file to process this request. Files and extracted text are handled according to your workspace retention and privacy settings.

Keep it clear.

Users should not need to decode mystery data practices.

Common mistakes

MistakeBetter approach
OCRing every PDF blindlyCheck for embedded text first
Using one OCR tool for every documentRoute by file type and complexity
Skipping preprocessingClean bad images before OCR
Throwing away page metadataPreserve pages, lines, and boxes when useful
Trusting OCR text blindlyAdd quality checks and review flags
Asking LLMAPI to read images directly when OCR is enoughOCR first, then structure text
Letting LLMAPI invent missing fieldsUse strict prompts and validation
No schema validationUse Pydantic
No raw OCR storageKeep raw text for debugging
No privacy planTreat documents as sensitive
Processing large PDFs synchronouslyUse background jobs
Returning giant messy OCR blobsClean, structure, and summarize

The sneakiest mistake is building a beautiful extraction UI on top of weak OCR and no review path.

The UI will look confident.

The data will not.

How this becomes a product feature

Once the OCR pipeline works, the feature can grow in different directions.

For a receipt app:

  • Extract merchant, date, total, tax, and line items.
  • Let users confirm unclear totals.
  • Export to expense tools.

For an invoice app:

  • Extract vendor, invoice number, due date, total, and payment terms.
  • Route missing totals to review.
  • Sync approved data to accounting.

For a document search app:

  • OCR every page.
  • Store page text.
  • Generate embeddings.
  • Let users search scanned PDFs.

For a support app:

  • Read uploaded screenshots.
  • Extract error messages.
  • Summarize what the screenshot shows.
  • Attach useful text to the support ticket.

For a legal/admin app:

  • Extract clauses or form fields.
  • Preserve page references.
  • Add reviewer notes.
  • Avoid unsupported guesses.

Same OCR base.

Different product layer.

Where LLMAPI helps most

LLMAPI is most useful after text has been extracted.

Use it for:

NeedLLMAPI role
Clean OCR textNormalize messy line breaks and obvious OCR artifacts
Extract fieldsTurn text into invoice, receipt, form, or ID schemas
Summarize documentsCreate short summaries from scanned pages
Classify documentsReceipt, invoice, contract, form, letter
Create search metadataTags, descriptions, key entities
Review warningsExplain missing or unclear fields
Transform outputMarkdown, JSON, CRM notes, database records
Validate meaningCheck whether required values are present
Human review notesTell reviewers what needs attention

A reliable workflow:

image / scan / PDF
→ OCR
→ cleaned text
→ LLMAPI structure or summary
→ validation
→ review if needed

This keeps the model grounded in actual extracted text.

Closing notes for builders

Image text extraction gets good when the app stops pretending all documents are clean.

Some files have embedded text. Some need OCR. Some need preprocessing. Some need layout-aware document analysis. Some need human review. Some should never be sent to a third-party OCR provider without a privacy review.

So build the pipeline with options.

Use Tesseract or PaddleOCR when local OCR is enough. Use Google Cloud Vision, Azure Document Intelligence, or Amazon Textract when the document mess is bigger than local OCR wants to handle. Use LLMAPI when raw OCR needs to become structured fields, summaries, tags, warnings, or clean app-ready output.

That is how a Python app goes from “please type this manually” to “upload the scan and let the system read it properly.”

Deploy in minutes