An OCR processing pipeline using VLM API
3
stars
180
commits
Python
primary language
Dec 19, 2025
updated
A unified OCR processing pipeline that leverages Vision Language Models (VLMs) for document layout detection, text extraction, and AI-powered text correction. This system processes images and PDFs locally using multiple VLM backends (OpenAI/OpenRouter, Gemini).
Based on: This project is based on and modified from Versatile-OCR-Program
π Full Documentation (once GitHub Pages is enabled)
Quick links:
Local documentation:
# Serve documentation locally
uv run mkdocs serve
# Visit http://127.0.0.1:8000
vlm-ocr-pipeline/
βββ main.py # CLI entry point
βββ pipeline/ # Modular VLM OCR Pipeline
β βββ __init__.py # Main Pipeline class
β βββ types.py # Integer-based BBox and Block types
β βββ constants.py
β βββ misc.py
β βββ prompt.py
β β
β βββ stages/ # 8-stage pipeline architecture
β β βββ __init__.py
β β βββ input_stage.py # Stage 1: Document loading & auxiliary info
β β βββ detection_stage.py # Stage 2: Layout block detection
β β βββ ordering_stage.py # Stage 3: Reading order analysis
β β βββ recognition_stage.py # Stage 4: Text extraction from blocks
β β βββ block_correction_stage.py # Stage 5: Block-level correction
β β βββ rendering_stage.py # Stage 6: Markdown/plaintext conversion
β β βββ page_correction_stage.py # Stage 7: Page-level VLM correction
β β βββ output_stage.py # Stage 8: Result saving & summary
β β
β βββ layout/
β β βββ detection/ # Layout detection strategies
β β β βββ __init__.py # create_detector()
β β β βββ doclayout_yolo.py # This project's DocLayout-YOLO
β β β βββ paddleocr/ # PaddleOCR detectors
β β β β βββ detector.py # PP-DocLayoutV2
β β β βββ mineru/ # MinerU detectors
β β β βββ doclayout_yolo.py # MinerU's DocLayout-YOLO
β β β βββ vlm.py # MinerU VLM
β β β
β β βββ ordering/ # Reading order strategies
β β βββ __init__.py # create_sorter(), validate_combination()
β β βββ pymupdf/ # PyMuPDF sorters
β β β βββ multi_column.py # Multi-column detection & sorting
β β βββ mineru/ # MinerU sorters
β β β βββ layoutreader.py # LayoutLMv3
β β β βββ xycut.py # XY-Cut algorithm
β β β βββ vlm.py # VLM ordering
β β βββ olmocr/ # olmOCR sorters
β β βββ vlm.py # VLM ordering
β β
β βββ conversion/ # PDF/Image conversion
β β βββ converter.py
β β
β βββ checkpoint/ # Smart resume functionality
β β βββ __init__.py
β β βββ progress.py # ProgressTracker for state management
β β βββ serializer.py # JSON serialization for checkpoints
β β
β βββ recognition/ # Text recognition and correction
β βββ __init__.py # TextRecognizer
β βββ cache.py
β βββ paddleocr/ # PaddleOCR recognizers
β β βββ paddleocr_vl.py # PaddleOCR-VL-0.9B
β βββ deepseek/ # DeepSeek-OCR recognizers
β β βββ deepseek_ocr.py # DeepSeek-OCR
β βββ api/ # VLM API clients (OpenAI, Gemini)
β
βββ models/
β βββ doclayout_yolo.py # DocLayout-YOLO wrapper
β
βββ external/ # External frameworks (git submodules)
β βββ MinerU/ # MinerU 2.5
β βββ olmocr/ # olmOCR
β βββ PaddleOCR/ # PaddleOCR v3.3.0 (PP-DocLayoutV2)
β βββ PaddleX/ # PaddleX v3.3.1 (PaddleOCR-VL-0.9B)
β βββ DeepSeek-OCR/ # DeepSeek-OCR (contextual optical compression)
β
βββ settings/
β βββ prompts/ # YAML prompt templates by model
β
βββ tests/ # Unit tests
βββ requirements.txt
βββ README.md # This file
βββ BBOX_FORMATS.md # BBox format reference
β
βββ .tmp/ # Temporary files (auto-created)
βββ .cache/ # Recognition cache (auto-created)
βββ .logs/ # Log files (auto-created)
βββ output/ # Processing results (auto-created)
The VLM OCR Pipeline uses a 8-stage architecture for document processing. Each stage has a clear responsibility and can be independently tested and modified.
graph TD
A[π Input Stage] --> B[π Detection Stage]
B --> C[π Ordering Stage]
C --> D[π Recognition Stage]
D --> E[βοΈ Block Correction Stage]
E --> F[π Rendering Stage]
F --> G[π§ Page Correction Stage]
G --> H[πΎ Output Stage]
A -->|PDF/Image| A1[Load document<br/>Extract auxiliary info]
B -->|numpy array| B1[Detect layout blocks<br/>bbox, type, confidence]
C -->|blocks list| C1[Sort by reading order<br/>Add order & column_index]
D -->|sorted blocks| D1[Extract text from blocks<br/>VLM or local model]
E -->|blocks with text| E1[Block-level correction<br/>Optional, disabled by default]
F -->|corrected blocks| F1[Convert to Markdown<br/>or plaintext]
G -->|raw text| G1[Page-level VLM correction<br/>Improve overall quality]
H -->|corrected text| H1[Save Page object<br/>Generate summary]
style A fill:#e1f5ff
style B fill:#fff3e1
style C fill:#e8f5e9
style D fill:#f3e5f5
style E fill:#fce4ec
style F fill:#fff9e1
style G fill:#e0f2f1
style H fill:#f1f8e9
Input Stage (InputStage)
Detection Stage (DetectionStage)
Ordering Stage (OrderingStage)
order field to blocks for correct reading sequencecolumn_index for multi-column documentsRecognition Stage (RecognitionStage)
Block Correction Stage (BlockCorrectionStage)
text to corrected_textRendering Stage (RenderingStage)
Page Correction Stage (PageCorrectionStage)
Output Stage (OutputStage)
Page objects with all metadata# Each stage processes data sequentially using unified process() method
page_image = input_stage.load_pdf_page(pdf_path, page_num)
blocks = detection_stage.process(page_image)
sorted_blocks = ordering_stage.process(blocks, image=page_image)
processed_blocks = recognition_stage.process(sorted_blocks, image=page_image)
processed_blocks = block_correction_stage.process(processed_blocks) # Optional, disabled by default
text = rendering_stage.process(processed_blocks, auxiliary_info=auxiliary_info)
result = page_correction_stage.process(text, page_num=page_num) # Optional, disabled by default
page_result = output_stage.build_page_result(...)
output_stage.save_page_output(output_dir, page_num, page_result)
This project integrates multiple frameworks (DocLayout-YOLO, MinerU, PyMuPDF, PyPDF, olmOCR), each using different bounding box formats. We provide a unified BBox conversion system that handles all formats automatically.
| Framework | Format | Coordinate Order | Origin | Example |
|---|---|---|---|---|
| Current Project (Internal) | BBox(x0, y0, x1, y1) | Top-Left + Bottom-Right (int) | Top-Left (0,0) | BBox(100, 50, 300, 200) |
| Current Project (JSON) | [x, y, w, h] | Top-Left + Size | Top-Left (0,0) | [100, 50, 200, 150] |
| YOLO | [x1, y1, x2, y2] | Top-Left + Bottom-Right | Top-Left (0,0) | [100, 50, 300, 200] |
| MinerU | [x0, y0, x1, y1] | Top-Left + Bottom-Right | Top-Left (0,0) | [100, 50, 300, 200] |
| PyMuPDF | Rect(x0, y0, x1, y1) | Top-Left + Bottom-Right | Top-Left (0,0) | Rect(100, 50, 300, 200) |
| PyPDF β οΈ | [x0, y0, x1, y1] | Bottom-Left + Top-Right β οΈ | Bottom-Left (0,0) β οΈ | [100, 592, 300, 742] |
| olmOCR | "[x, y]text" | Text format | Top-Left (0,0) | "[100x50]Chapter 1" |
Key Points:
BBox(x0, y0, x1, y1) with integer coordinates (xyxy)[x, y, w, h] (xywh) for human readabilityExample Conversion:
from pipeline.types import BBox, Block
# Create bbox (accepts float, converts to int)
bbox = BBox.from_xywh(100, 50, 200, 150) # [x, y, w, h] β BBox(100, 50, 300, 200)
bbox = BBox.from_xyxy(100, 50, 300, 200) # [x0, y0, x1, y1] β BBox(100, 50, 300, 200)
bbox = BBox.from_cxcywh(200, 125, 200, 150) # Center format
# Internal: integer xyxy
print(bbox.x0, bbox.y0, bbox.x1, bbox.y1) # 100 50 300 200
# Convert to formats
mineru = bbox.to_mineru_bbox() # [100, 50, 300, 200] (xyxy)
json_bbox = bbox.to_xywh_list() # [100, 50, 200, 150] (xywh for JSON)
anchor = bbox.to_olmocr_anchor("image") # "[Image 100x50 to 300x200]"
# Direct image cropping
cropped = bbox.crop(image, padding=5)
# Block usage
block = Block(type="text", bbox=bbox, detection_confidence=0.95)
data = region.to_dict() # {"type": "text", "bbox": [100, 50, 200, 150], ...}
For detailed format specifications and conversion examples, see BBox Format Reference.
# Clone or download the project
cd vlm-ocr-pipeline
# Create virtual environment (recommended with Python 3.11 for best compatibility)
uv venv --python 3.11 .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
uv pip install -r requirements.txt
Run setup script to fix DocLayout-YOLO compatibility issues
python setup.py
export GEMINI_API_KEY="your_api_key_here"
Vision Language Models are advanced AI systems that can understand both visual and textual information simultaneously. Unlike traditional OCR that simply extracts text, VLMs can:
Popular VLMs supported by this pipeline include:
For OpenAI backend:
export OPENAI_API_KEY="your_openai_api_key_here"
For OpenRouter backend (supports multiple models including Gemini):
export OPENROUTER_API_KEY="your_openrouter_api_key_here"
Create a .env file in the project root:
# Choose your preferred backend (openai is default)
GEMINI_API_KEY=your_gemini_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
OPENROUTER_API_KEY=your_openrouter_api_key_here
# Optional: Custom OpenAI base URL (for OpenRouter or other compatible services)
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
VLM OCR Pipeline uses YAML configuration files in the settings/ directory to customize behavior without modifying code.
All configuration files are in settings/:
models.yaml: Default model paths (e.g., opendatalab/MinerU2.5-2509-1.2B for mineru-vlm detector)detection_config.yaml: Detector parameters
confidence_threshold: Detection sensitivity (0.0-1.0, default: 0.5)nms_threshold: Non-Maximum Suppression for overlapping boxes (default: 0.45)ordering_config.yaml: Sorter parameters
overlap_threshold: IoU threshold for duplicate removal (default: 0.7)temperature, max_new_tokens: VLM sorter settingsapi_config.yaml: API client parameters
max_tokens: Response length limit (default: 2000)temperature: Sampling temperature (0.0 = deterministic, default: 0.1)estimated_tokens: For Gemini rate limitingAdjust detection sensitivity:
# settings/detection_config.yaml
detectors:
doclayout-yolo:
confidence_threshold: 0.7 # Stricter (default: 0.5)
Customize API parameters:
# settings/api_config.yaml
openai:
text_extraction:
max_tokens: 3000 # Longer responses (default: 2000)
temperature: 0.0 # Fully deterministic (default: 0.1)
settings/pipeline/constants.pyMissing configuration files automatically fall back to safe defaults with a warning.
The pipeline supports flexible inference backends for each processing stage (detector, sorter, recognizer). Backends can be auto-selected or explicitly specified for performance optimization.
| Backend | Description | Use Cases |
|---|---|---|
pytorch | Native PyTorch inference (single GPU) | DocLayout-YOLO, PaddleOCR models |
hf | HuggingFace Transformers (single GPU) | MinerU VLM, olmOCR, LayoutReader |
vllm | vLLM inference engine (high-throughput) | MinerU VLM, olmOCR, PaddleOCR-VL |
sglang | SGLang inference engine (structured generation) | PaddleOCR-VL |
openai | OpenAI API | GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo |
gemini | Google Gemini API | Gemini 2.5 Flash, Gemini 2.0 Pro |
Auto-selection (recommended):
# Backends are automatically selected based on model capabilities
python main.py --input document.pdf --detector mineru-vlm --recognizer gpt-4o
# detector backend: hf (HuggingFace Transformers)
# recognizer backend: openai (OpenAI API)
Explicit backend specification:
# Specify backends for performance tuning
python main.py --input document.pdf \
--detector mineru-vlm --detector-backend vllm \
--sorter olmocr-vlm --sorter-backend vllm \
--recognizer paddleocr-vl --recognizer-backend sglang
# DocLayout-YOLO (PyTorch only)
python main.py --input doc.pdf --detector doclayout-yolo
# MinerU VLM (HuggingFace or vLLM)
python main.py --input doc.pdf --detector mineru-vlm --detector-backend hf
python main.py --input doc.pdf --detector mineru-vlm --detector-backend vllm
# PaddleOCR PP-DocLayoutV2 (PaddlePaddle only)
python main.py --input doc.pdf --detector paddleocr-doclayout-v2
# Algorithm-based sorters (no backend)
python main.py --input doc.pdf --sorter pymupdf
python main.py --input doc.pdf --sorter mineru-xycut
# olmOCR VLM (HuggingFace or vLLM)
python main.py --input doc.pdf --sorter olmocr-vlm --sorter-backend hf
python main.py --input doc.pdf --sorter olmocr-vlm --sorter-backend vllm
# API-based recognizers (OpenAI, Gemini)
python main.py --input doc.pdf --recognizer gpt-4o
python main.py --input doc.pdf --recognizer gemini-2.5-flash
# Local PaddleOCR-VL (PyTorch, vLLM, or SGLang)
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend pytorch
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend vllm
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend sglang
# Local DeepSeek-OCR (HuggingFace or vLLM)
python main.py --input doc.pdf --recognizer deepseek-ocr --recognizer-backend hf
python main.py --input doc.pdf --recognizer deepseek-ocr --recognizer-backend vllm
| Model | Supported Backends | Default |
|---|---|---|
| Detectors | ||
doclayout-yolo | (native PyTorch) | - |
mineru-doclayout-yolo | (native PyTorch) | - |
mineru-vlm | hf, vllm | hf |
paddleocr-doclayout-v2 | (native PaddlePaddle) | - |
| Sorters | ||
pymupdf | (rule-based) | - |
mineru-xycut | (algorithm-based) | - |
mineru-layoutreader | (HuggingFace Transformers) | - |
mineru-vlm | (uses detector backend) | - |
olmocr-vlm | hf, vllm | hf |
paddleocr-doclayout-v2 | (passthrough) | - |
| Recognizers | ||
openai (GPT models) | openai | openai |
gemini (Gemini models) | gemini | gemini |
paddleocr-vl | pytorch, vllm, sglang | pytorch |
deepseek-ocr | hf, vllm | hf |
For detailed backend configuration, see settings/models.yaml.
# Process a single PDF (uses default: paddleocr-doclayout-v2 detector + paddleocr-vl recognizer, balanced DPI)
python main.py --input document.pdf
# Use different recognizer
python main.py --input document.pdf --recognizer deepseek-ocr
python main.py --input document.pdf --recognizer gemini-2.5-flash
python main.py --input document.pdf --recognizer gpt-4o
# Process a directory of PDFs
python main.py --input /path/to/pdfs/
# Process a single image
python main.py --input image.jpg
# Specify custom output directory
python main.py --input document.pdf --output /custom/output/
# Disable caching for fresh processing
python main.py --input document.pdf --no-cache
# Use custom prompts directory (overrides auto-detection)
python main.py --input document.pdf --prompts-dir custom_prompts/
# Backend and model combinations
python main.py --input document.pdf --backend openai --model gpt-4o-mini
python main.py --input document.pdf --backend gemini --model gemini-2.5-flash
# Page limiting options (mutually exclusive)
python main.py --input document.pdf --max-pages 5
python main.py --input document.pdf --page-range 10-20
python main.py --input document.pdf --pages 1,3,5,10,15
# Adjust detection confidence threshold
python main.py --input document.pdf --confidence 0.7
# Use custom model path
python main.py --input document.pdf --model-path /path/to/custom/model.pt
# DPI configuration for PDF-to-image conversion
python main.py --input document.pdf --dpi fast # 150 DPI - fastest
python main.py --input document.pdf --dpi balanced # 150β300 dual (recommended)
python main.py --input document.pdf --dpi quality # 300 DPI - best quality
python main.py --input document.pdf --dpi 200 # Custom single DPI
python main.py --input document.pdf --dpi 150,300 # Custom dual DPI (detection,recognition)
# Enable debug logging
python main.py --input document.pdf --log-level DEBUG
# Text correction options (disabled by default)
python main.py --input document.pdf --block-correction # Enable block-level VLM correction
python main.py --input document.pdf --page-correction # Enable page-level VLM correction
python main.py --input document.pdf --block-correction --page-correction # Enable both
# Combined advanced usage
python main.py --input /docs/ --max-pages 3 --confidence 0.8 --dpi 250
# Use different detector + sorter combinations
python main.py --input document.pdf --detector doclayout-yolo --sorter mineru-xycut
# Use PaddleOCR PP-DocLayoutV2 detector with XY-Cut sorter
python main.py --input document.pdf --detector paddleocr-doclayout-v2 --sorter mineru-xycut
# Use MinerU VLM for both detection and ordering (tightly coupled)
python main.py --input document.pdf --detector mineru-vlm --sorter mineru-vlm \
--mineru-model opendatalab/PDF-Extract-Kit-1.0
# Use multi-column aware PyMuPDF sorter
python main.py --input document.pdf --detector doclayout-yolo --sorter pymupdf
The pipeline automatically saves progress after processing each page. If processing is interrupted (error, Ctrl+C, rate limit), simply re-run the same command and it will automatically resume from where it left off.
# First run (fails at page 5 due to error or interruption)
python main.py --input document.pdf --output results/
# Creates: results/_progress.json (progress tracker)
# results/stage6_output_page1.json, page2.json, ... (checkpoints)
# Re-run (automatically resumes from page 5)
python main.py --input document.pdf --output results/
# Reads _progress.json β skips pages 1-4 β continues from page 5
# The system automatically:
# - Detects existing checkpoints in output directory
# - Validates input file matches
# - Skips already-processed pages
# - Displays resume information (last run time, completed stages, etc.)
How it works:
_progress.json tracks pipeline execution state (completed pages, timestamps, errors)--output directory# Use PaddleOCR PP-DocLayoutV2 detector + PaddleOCR-VL-0.9B recognizer
# This provides a complete PaddleOCR-based pipeline with 109 language support
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--sorter mineru-xycut
# With custom backend (vLLM or SGLang for acceleration)
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--paddleocr-vl-backend vllm-server \
--sorter mineru-xycut
# With custom query templates for specific block types
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--paddleocr-vl-query-table "Extract this table in markdown:" \
--sorter mineru-xycut
Available Detectors:
doclayout-yolo: This project's DocLayout-YOLO (default)paddleocr-doclayout-v2: PaddleOCR PP-DocLayoutV2 (25 categories, requires PaddleOCR v3.3.0)mineru-doclayout-yolo: MinerU's DocLayout-YOLO implementationmineru-vlm: MinerU VLM-based detectionAvailable Recognizers:
openai: OpenAI VLM backend (default, uses API)gemini: Gemini VLM backend (uses API)paddleocr-vl: PaddleOCR-VL-0.9B local model (109 languages, requires PaddleX v3.3.1)Available Sorters:
mineru-xycut: Fast XY-Cut algorithm (default, recommended)pymupdf: Multi-column aware sortingmineru-layoutreader: LayoutLMv3-based reading ordermineru-vlm: MinerU VLM-based ordering (requires mineru-vlm detector)olmocr-vlm: olmOCR VLM-based ordering
### Python API Usage
```python
from pipeline import Pipeline
# Initialize pipeline with default settings (Gemini API)
pipeline = Pipeline(
confidence_threshold=0.5,
use_cache=True,
cache_dir=".cache",
output_dir="output"
)
# Process a single image
result = pipeline.process_image("document.jpg")
print(f"Extracted text: {result['corrected_text']}")
# Process a PDF
result = pipeline.process_pdf("document.pdf")
print(f"Processed {result['num_pages']} pages")
# Process PDF with page limits
result = pipeline.process_pdf(
"document.pdf",
max_pages=5 # Process only first 5 pages
)
result = pipeline.process_pdf(
"document.pdf",
page_range=(10, 20) # Process pages 10-20
)
result = pipeline.process_pdf(
"document.pdf",
specific_pages=[1, 5, 10, 15] # Process specific pages
)
# Process a directory
result = pipeline.process_directory("input_folder/")
print(f"Processed {result['total_pdfs']} PDF files")
Each single image (or individual PDF page) is written as page_<number>.json under <output>/<model>/<document_stem>/. The payload includes:
image_path: Path to the rendered page image (or original image if supplied)width / height: Pixel dimensions of the rendered pageregions: Raw DocLayout-YOLO detections (bounding boxes and labels)processed_regions: Post-processed regions with extracted text, table summaries, etc.raw_text: Natural reading-order text composed from text-like regionscorrected_text: Text after VLM correction (falls back to raw_text on failure)correction_confidence: Similarity score between raw and corrected text (0β1)processing_time_seconds: Total latency spent on the pageprocessed_at: ISO-8601 timestamp for when processing completed{
"image_path": "output/tmp/document_page_1.jpg",
"width": 1920,
"height": 1080,
"blocks": [...],
"processed_regions": [...],
"raw_text": "Original OCR text...",
"corrected_text": "AI-corrected text...",
"correction_confidence": 0.95,
"processing_time_seconds": 12.34,
"processed_at": "2024-12-19T10:30:00"
}
PDF runs emit a summary file alongside the page outputs: summary.json (all pages succeeded), summary_partial.json (some failures), or summary_incomplete.json (stopped early). The schema captures:
pdf_name / pdf_path: Original filename and absolute pathnum_pages: Number of pages in the source PDFprocessed_pages: Count of pages processed (including fallbacks)output_directory: Folder that contains per-page and summary JSON artifactsprocessed_at: ISO-8601 timestamp for completionstatus_summary: Totals of complete, partial, and incomplete pagespages: Array of page status objects with optional file suffix (e.g., partial β page_2_partial.json)processing_stopped: Indicates an early stop due to rate limits or unexpected errors{
"pdf_name": "document",
"pdf_path": "/path/to/document.pdf",
"num_pages": 10,
"processed_pages": 10,
"output_directory": "output/gemini-2.5-flash/document",
"processed_at": "2024-12-19T10:30:00",
"status_summary": {"complete": 10},
"pages": [
{"page": 1, "status": "complete", "file_suffix": ""},
{"page": 2, "status": "partial", "file_suffix": "partial"}
],
"processing_stopped": false
}
Convert JSON output to Markdown format using two conversion strategies:
__init__.py)Simple and fast conversion using pre-classified block types:
from pipeline.conversion.output.markdown import json_to_markdown
regions = [
{"type": "title", "text": "Document Title"},
{"type": "subtitle", "text": "Section 1"},
{"type": "text", "text": "Content here."},
]
md = json_to_markdown(regions)
# # Document Title
#
# ## Section 1
#
# Content here.
Features:
pymupdf4llm.py)Advanced conversion using font size information from PDF text spans (PyMuPDF parser):
import json
from pathlib import Path
from pipeline.conversion.output.markdown.pymupdf4llm import to_markdown
# Load page result with auxiliary_info
with open("output/model/document/page_1.json") as f:
page_result = json.load(f)
# page_result contains:
# {
# "processed_regions": [...],
# "auxiliary_info": {
# "text_spans": [ # PDF text objects with font info
# {"bbox": [100, 50, 300, 80], "text": "Chapter 1", "size": 24.0, "font": "Times-Bold"}
# ]
# }
# }
# Auto-detect headers from font sizes and convert
md = to_markdown(page_result, auto_detect_headers=True)
# # Chapter 1 β 24pt β H1 (largest)
#
# ## Section 1.1 β 18pt β H2 (2nd largest)
#
# Body text. β 12pt β body text
Key Concepts:
auxiliary_info.text_spanssize and font (not font_size, font_name)How It Works:
1. PDF β Detector β Blocks (layout detection from image)
2. PDF β PyMuPDF Parser β Text Spans (font info from digital document)
3. Both saved separately in JSON (auxiliary_info)
4. Markdown conversion β IoU matching β Font-based headers
Comparison:
| Feature | Block Type-Based | Font Size-Based |
|---|---|---|
| Speed | β‘ Fast | π Slower (PDF parsing) |
| Accuracy | Layout detection dependent | Font size dependent |
| Dependencies | None | PyMuPDF (fitz) |
| Data Source | Block classification | PDF text spans |
| Use Case | Default, quick conversion | Precise header detection |
All text extraction is performed by the configured VLM backend (Gemini by default, or OpenAI/OpenRouter if selected). The model receives both rendered page images and prompt instructions tailored to the backend. Rate limiting and caching ensure the pipeline stays within API quotas while avoiding repeated work on identical regions.
For testing purposes, cost control, or processing specific sections, you can limit which pages to process:
--max-pages)Process only the first N pages from the beginning:
python main.py --input document.pdf --max-pages 5
--page-range)Process a specific range of pages:
python main.py --input document.pdf --page-range 10-20
python main.py --input document.pdf --page-range 1-5
--pages)Process only specified pages (comma-separated):
python main.py --input document.pdf --pages 1,5,10,15
python main.py --input document.pdf --pages 3,7,12
--max-pages 1 to test with just the first page before processing entire documents--max-pages 1 to test pipeline with single page--max-pages 10 to limit API calls for large documents--page-range 5-15 to process only content pages--pages 1,2 to process only first few pages--pages 10,25,50 to process sample pagesPrompts are organized by model family for optimal results. The system automatically selects the appropriate prompt directory based on the backend and model:
settings/prompts/
βββ gemini/ # Gemini-specific prompts
β βββ text_extraction.yaml
β βββ content_analysis.yaml
β βββ text_correction.yaml
βββ openai/ # OpenAI/GPT-specific prompts
β βββ text_extraction.yaml
β βββ content_analysis.yaml
β βββ text_correction.yaml
βββ internvl/ # InternVL-specific prompts
βββ qwen/ # Qwen-specific prompts
βββ phi4/ # Phi-4-specific prompts
The system automatically detects the appropriate prompt directory:
--backend gemini β settings/prompts/gemini/--backend openai --model gpt-4o β settings/prompts/openai/--backend openai --model google/gemini-2.5-flash β settings/prompts/gemini/--model internvl/internvl2-5 β settings/prompts/internvl/cp -r settings/prompts/gemini custom_promptspython main.py --input doc.pdf --prompts-dir custom_prompts/# Example: settings/prompts/text_extraction.yaml
text_extraction:
system: |
You are an expert OCR system...
user: |
Please extract all text from this image...
fallback: |
Extract all visible text accurately...
Tables are automatically detected and processed with structured analysis:
[TableStart]
## Table Structure:
| Column1 | Column2 | Column3 |
|---------|---------|---------|
| Data1 | Data2 | Data3 |
## Summary:
Brief description of table content
## Educational Significance:
Importance and context
## Related Topics:
Topic1, Topic2, Topic3
[TableEnd]
Figures and images receive detailed analysis:
[FigureStart]
## Image Description:
Detailed description of visual content
## Educational Significance:
Educational importance
## Related Topics:
Related learning topics
## Exam Relevance:
How this could be used in exams
[FigureEnd]
--cache option (default) to avoid reprocessingThe OCR Pipeline automatically creates detailed log files with timestamps for tracking and debugging.
.logs/ (hidden directory, auto-created)YYYY-MM-DD_HH-MM-SS_ocr_pipeline.log.logs/
βββ 2025-07-29_18-06-53_ocr_pipeline.log # Rate limit status check
βββ 2025-07-29_18-07-23_ocr_pipeline.log # Full OCR processing run
βββ 2025-07-29_19-15-42_ocr_pipeline.log # Another processing session
--log-level DEBUG)# Standard logging (INFO level)
python main.py --input document.pdf
# Detailed debugging logs
python main.py --input document.pdf --log-level DEBUG
# Minimal logging (ERROR only)
python main.py --input document.pdf --log-level ERROR
Log files are automatically organized by timestamp but not automatically deleted. You can:
# View recent logs
ls -la .logs/
# Remove old logs (older than 7 days)
find .logs/ -name "*.log" -mtime +7 -delete
# Archive logs by month
mkdir -p .logs/archive/2025-07/
mv .logs/2025-07-* .logs/archive/2025-07/
# Ensure you're in the project root directory
cd gemini_ocr
python main.py --input document.pdf
# Verify environment variable
echo $GEMINI_API_KEY
# Or check .env file
cat .env
# Check PyTorch CUDA installation
python -c "import torch; print(torch.cuda.is_available())"
# CPU-only operation is supported if GPU unavailable
The pipeline follows a modular design:
# In ocr_pipeline.py
def _get_gemini_prompt_for_region_type(self, region_type: str) -> str:
if region_type == 'new_type':
return "Custom prompt for new block type..."
# ... existing code
# Inherit from Pipeline
class CustomPipeline(Pipeline):
def _correct_text_with_gemini(self, text: str) -> Dict[str, Any]:
# Custom correction logic
return super()._correct_text_with_gemini(text)
This project is for educational and research purposes. Please ensure compliance with the respective API provider's terms of service and usage limits.
For issues and questions:
GEMINI_API_KEY, OPENAI_API_KEY).logs/Note: This system requires internet connectivity for API calls to the selected VLM provider. All processing results are stored locally for privacy and offline access.
180 commits
Python
99.7%
An OCR processing pipeline using VLM API
3
stars
180
commits
Python
primary language
Dec 19, 2025
updated
A unified OCR processing pipeline that leverages Vision Language Models (VLMs) for document layout detection, text extraction, and AI-powered text correction. This system processes images and PDFs locally using multiple VLM backends (OpenAI/OpenRouter, Gemini).
Based on: This project is based on and modified from Versatile-OCR-Program
π Full Documentation (once GitHub Pages is enabled)
Quick links:
Local documentation:
# Serve documentation locally
uv run mkdocs serve
# Visit http://127.0.0.1:8000
vlm-ocr-pipeline/
βββ main.py # CLI entry point
βββ pipeline/ # Modular VLM OCR Pipeline
β βββ __init__.py # Main Pipeline class
β βββ types.py # Integer-based BBox and Block types
β βββ constants.py
β βββ misc.py
β βββ prompt.py
β β
β βββ stages/ # 8-stage pipeline architecture
β β βββ __init__.py
β β βββ input_stage.py # Stage 1: Document loading & auxiliary info
β β βββ detection_stage.py # Stage 2: Layout block detection
β β βββ ordering_stage.py # Stage 3: Reading order analysis
β β βββ recognition_stage.py # Stage 4: Text extraction from blocks
β β βββ block_correction_stage.py # Stage 5: Block-level correction
β β βββ rendering_stage.py # Stage 6: Markdown/plaintext conversion
β β βββ page_correction_stage.py # Stage 7: Page-level VLM correction
β β βββ output_stage.py # Stage 8: Result saving & summary
β β
β βββ layout/
β β βββ detection/ # Layout detection strategies
β β β βββ __init__.py # create_detector()
β β β βββ doclayout_yolo.py # This project's DocLayout-YOLO
β β β βββ paddleocr/ # PaddleOCR detectors
β β β β βββ detector.py # PP-DocLayoutV2
β β β βββ mineru/ # MinerU detectors
β β β βββ doclayout_yolo.py # MinerU's DocLayout-YOLO
β β β βββ vlm.py # MinerU VLM
β β β
β β βββ ordering/ # Reading order strategies
β β βββ __init__.py # create_sorter(), validate_combination()
β β βββ pymupdf/ # PyMuPDF sorters
β β β βββ multi_column.py # Multi-column detection & sorting
β β βββ mineru/ # MinerU sorters
β β β βββ layoutreader.py # LayoutLMv3
β β β βββ xycut.py # XY-Cut algorithm
β β β βββ vlm.py # VLM ordering
β β βββ olmocr/ # olmOCR sorters
β β βββ vlm.py # VLM ordering
β β
β βββ conversion/ # PDF/Image conversion
β β βββ converter.py
β β
β βββ checkpoint/ # Smart resume functionality
β β βββ __init__.py
β β βββ progress.py # ProgressTracker for state management
β β βββ serializer.py # JSON serialization for checkpoints
β β
β βββ recognition/ # Text recognition and correction
β βββ __init__.py # TextRecognizer
β βββ cache.py
β βββ paddleocr/ # PaddleOCR recognizers
β β βββ paddleocr_vl.py # PaddleOCR-VL-0.9B
β βββ deepseek/ # DeepSeek-OCR recognizers
β β βββ deepseek_ocr.py # DeepSeek-OCR
β βββ api/ # VLM API clients (OpenAI, Gemini)
β
βββ models/
β βββ doclayout_yolo.py # DocLayout-YOLO wrapper
β
βββ external/ # External frameworks (git submodules)
β βββ MinerU/ # MinerU 2.5
β βββ olmocr/ # olmOCR
β βββ PaddleOCR/ # PaddleOCR v3.3.0 (PP-DocLayoutV2)
β βββ PaddleX/ # PaddleX v3.3.1 (PaddleOCR-VL-0.9B)
β βββ DeepSeek-OCR/ # DeepSeek-OCR (contextual optical compression)
β
βββ settings/
β βββ prompts/ # YAML prompt templates by model
β
βββ tests/ # Unit tests
βββ requirements.txt
βββ README.md # This file
βββ BBOX_FORMATS.md # BBox format reference
β
βββ .tmp/ # Temporary files (auto-created)
βββ .cache/ # Recognition cache (auto-created)
βββ .logs/ # Log files (auto-created)
βββ output/ # Processing results (auto-created)
The VLM OCR Pipeline uses a 8-stage architecture for document processing. Each stage has a clear responsibility and can be independently tested and modified.
graph TD
A[π Input Stage] --> B[π Detection Stage]
B --> C[π Ordering Stage]
C --> D[π Recognition Stage]
D --> E[βοΈ Block Correction Stage]
E --> F[π Rendering Stage]
F --> G[π§ Page Correction Stage]
G --> H[πΎ Output Stage]
A -->|PDF/Image| A1[Load document<br/>Extract auxiliary info]
B -->|numpy array| B1[Detect layout blocks<br/>bbox, type, confidence]
C -->|blocks list| C1[Sort by reading order<br/>Add order & column_index]
D -->|sorted blocks| D1[Extract text from blocks<br/>VLM or local model]
E -->|blocks with text| E1[Block-level correction<br/>Optional, disabled by default]
F -->|corrected blocks| F1[Convert to Markdown<br/>or plaintext]
G -->|raw text| G1[Page-level VLM correction<br/>Improve overall quality]
H -->|corrected text| H1[Save Page object<br/>Generate summary]
style A fill:#e1f5ff
style B fill:#fff3e1
style C fill:#e8f5e9
style D fill:#f3e5f5
style E fill:#fce4ec
style F fill:#fff9e1
style G fill:#e0f2f1
style H fill:#f1f8e9
Input Stage (InputStage)
Detection Stage (DetectionStage)
Ordering Stage (OrderingStage)
order field to blocks for correct reading sequencecolumn_index for multi-column documentsRecognition Stage (RecognitionStage)
Block Correction Stage (BlockCorrectionStage)
text to corrected_textRendering Stage (RenderingStage)
Page Correction Stage (PageCorrectionStage)
Output Stage (OutputStage)
Page objects with all metadata# Each stage processes data sequentially using unified process() method
page_image = input_stage.load_pdf_page(pdf_path, page_num)
blocks = detection_stage.process(page_image)
sorted_blocks = ordering_stage.process(blocks, image=page_image)
processed_blocks = recognition_stage.process(sorted_blocks, image=page_image)
processed_blocks = block_correction_stage.process(processed_blocks) # Optional, disabled by default
text = rendering_stage.process(processed_blocks, auxiliary_info=auxiliary_info)
result = page_correction_stage.process(text, page_num=page_num) # Optional, disabled by default
page_result = output_stage.build_page_result(...)
output_stage.save_page_output(output_dir, page_num, page_result)
This project integrates multiple frameworks (DocLayout-YOLO, MinerU, PyMuPDF, PyPDF, olmOCR), each using different bounding box formats. We provide a unified BBox conversion system that handles all formats automatically.
| Framework | Format | Coordinate Order | Origin | Example |
|---|---|---|---|---|
| Current Project (Internal) | BBox(x0, y0, x1, y1) | Top-Left + Bottom-Right (int) | Top-Left (0,0) | BBox(100, 50, 300, 200) |
| Current Project (JSON) | [x, y, w, h] | Top-Left + Size | Top-Left (0,0) | [100, 50, 200, 150] |
| YOLO | [x1, y1, x2, y2] | Top-Left + Bottom-Right | Top-Left (0,0) | [100, 50, 300, 200] |
| MinerU | [x0, y0, x1, y1] | Top-Left + Bottom-Right | Top-Left (0,0) | [100, 50, 300, 200] |
| PyMuPDF | Rect(x0, y0, x1, y1) | Top-Left + Bottom-Right | Top-Left (0,0) | Rect(100, 50, 300, 200) |
| PyPDF β οΈ | [x0, y0, x1, y1] | Bottom-Left + Top-Right β οΈ | Bottom-Left (0,0) β οΈ | [100, 592, 300, 742] |
| olmOCR | "[x, y]text" | Text format | Top-Left (0,0) | "[100x50]Chapter 1" |
Key Points:
BBox(x0, y0, x1, y1) with integer coordinates (xyxy)[x, y, w, h] (xywh) for human readabilityExample Conversion:
from pipeline.types import BBox, Block
# Create bbox (accepts float, converts to int)
bbox = BBox.from_xywh(100, 50, 200, 150) # [x, y, w, h] β BBox(100, 50, 300, 200)
bbox = BBox.from_xyxy(100, 50, 300, 200) # [x0, y0, x1, y1] β BBox(100, 50, 300, 200)
bbox = BBox.from_cxcywh(200, 125, 200, 150) # Center format
# Internal: integer xyxy
print(bbox.x0, bbox.y0, bbox.x1, bbox.y1) # 100 50 300 200
# Convert to formats
mineru = bbox.to_mineru_bbox() # [100, 50, 300, 200] (xyxy)
json_bbox = bbox.to_xywh_list() # [100, 50, 200, 150] (xywh for JSON)
anchor = bbox.to_olmocr_anchor("image") # "[Image 100x50 to 300x200]"
# Direct image cropping
cropped = bbox.crop(image, padding=5)
# Block usage
block = Block(type="text", bbox=bbox, detection_confidence=0.95)
data = region.to_dict() # {"type": "text", "bbox": [100, 50, 200, 150], ...}
For detailed format specifications and conversion examples, see BBox Format Reference.
# Clone or download the project
cd vlm-ocr-pipeline
# Create virtual environment (recommended with Python 3.11 for best compatibility)
uv venv --python 3.11 .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
uv pip install -r requirements.txt
Run setup script to fix DocLayout-YOLO compatibility issues
python setup.py
export GEMINI_API_KEY="your_api_key_here"
Vision Language Models are advanced AI systems that can understand both visual and textual information simultaneously. Unlike traditional OCR that simply extracts text, VLMs can:
Popular VLMs supported by this pipeline include:
For OpenAI backend:
export OPENAI_API_KEY="your_openai_api_key_here"
For OpenRouter backend (supports multiple models including Gemini):
export OPENROUTER_API_KEY="your_openrouter_api_key_here"
Create a .env file in the project root:
# Choose your preferred backend (openai is default)
GEMINI_API_KEY=your_gemini_api_key_here
OPENAI_API_KEY=your_openai_api_key_here
OPENROUTER_API_KEY=your_openrouter_api_key_here
# Optional: Custom OpenAI base URL (for OpenRouter or other compatible services)
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
VLM OCR Pipeline uses YAML configuration files in the settings/ directory to customize behavior without modifying code.
All configuration files are in settings/:
models.yaml: Default model paths (e.g., opendatalab/MinerU2.5-2509-1.2B for mineru-vlm detector)detection_config.yaml: Detector parameters
confidence_threshold: Detection sensitivity (0.0-1.0, default: 0.5)nms_threshold: Non-Maximum Suppression for overlapping boxes (default: 0.45)ordering_config.yaml: Sorter parameters
overlap_threshold: IoU threshold for duplicate removal (default: 0.7)temperature, max_new_tokens: VLM sorter settingsapi_config.yaml: API client parameters
max_tokens: Response length limit (default: 2000)temperature: Sampling temperature (0.0 = deterministic, default: 0.1)estimated_tokens: For Gemini rate limitingAdjust detection sensitivity:
# settings/detection_config.yaml
detectors:
doclayout-yolo:
confidence_threshold: 0.7 # Stricter (default: 0.5)
Customize API parameters:
# settings/api_config.yaml
openai:
text_extraction:
max_tokens: 3000 # Longer responses (default: 2000)
temperature: 0.0 # Fully deterministic (default: 0.1)
settings/pipeline/constants.pyMissing configuration files automatically fall back to safe defaults with a warning.
The pipeline supports flexible inference backends for each processing stage (detector, sorter, recognizer). Backends can be auto-selected or explicitly specified for performance optimization.
| Backend | Description | Use Cases |
|---|---|---|
pytorch | Native PyTorch inference (single GPU) | DocLayout-YOLO, PaddleOCR models |
hf | HuggingFace Transformers (single GPU) | MinerU VLM, olmOCR, LayoutReader |
vllm | vLLM inference engine (high-throughput) | MinerU VLM, olmOCR, PaddleOCR-VL |
sglang | SGLang inference engine (structured generation) | PaddleOCR-VL |
openai | OpenAI API | GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo |
gemini | Google Gemini API | Gemini 2.5 Flash, Gemini 2.0 Pro |
Auto-selection (recommended):
# Backends are automatically selected based on model capabilities
python main.py --input document.pdf --detector mineru-vlm --recognizer gpt-4o
# detector backend: hf (HuggingFace Transformers)
# recognizer backend: openai (OpenAI API)
Explicit backend specification:
# Specify backends for performance tuning
python main.py --input document.pdf \
--detector mineru-vlm --detector-backend vllm \
--sorter olmocr-vlm --sorter-backend vllm \
--recognizer paddleocr-vl --recognizer-backend sglang
# DocLayout-YOLO (PyTorch only)
python main.py --input doc.pdf --detector doclayout-yolo
# MinerU VLM (HuggingFace or vLLM)
python main.py --input doc.pdf --detector mineru-vlm --detector-backend hf
python main.py --input doc.pdf --detector mineru-vlm --detector-backend vllm
# PaddleOCR PP-DocLayoutV2 (PaddlePaddle only)
python main.py --input doc.pdf --detector paddleocr-doclayout-v2
# Algorithm-based sorters (no backend)
python main.py --input doc.pdf --sorter pymupdf
python main.py --input doc.pdf --sorter mineru-xycut
# olmOCR VLM (HuggingFace or vLLM)
python main.py --input doc.pdf --sorter olmocr-vlm --sorter-backend hf
python main.py --input doc.pdf --sorter olmocr-vlm --sorter-backend vllm
# API-based recognizers (OpenAI, Gemini)
python main.py --input doc.pdf --recognizer gpt-4o
python main.py --input doc.pdf --recognizer gemini-2.5-flash
# Local PaddleOCR-VL (PyTorch, vLLM, or SGLang)
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend pytorch
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend vllm
python main.py --input doc.pdf --recognizer paddleocr-vl --recognizer-backend sglang
# Local DeepSeek-OCR (HuggingFace or vLLM)
python main.py --input doc.pdf --recognizer deepseek-ocr --recognizer-backend hf
python main.py --input doc.pdf --recognizer deepseek-ocr --recognizer-backend vllm
| Model | Supported Backends | Default |
|---|---|---|
| Detectors | ||
doclayout-yolo | (native PyTorch) | - |
mineru-doclayout-yolo | (native PyTorch) | - |
mineru-vlm | hf, vllm | hf |
paddleocr-doclayout-v2 | (native PaddlePaddle) | - |
| Sorters | ||
pymupdf | (rule-based) | - |
mineru-xycut | (algorithm-based) | - |
mineru-layoutreader | (HuggingFace Transformers) | - |
mineru-vlm | (uses detector backend) | - |
olmocr-vlm | hf, vllm | hf |
paddleocr-doclayout-v2 | (passthrough) | - |
| Recognizers | ||
openai (GPT models) | openai | openai |
gemini (Gemini models) | gemini | gemini |
paddleocr-vl | pytorch, vllm, sglang | pytorch |
deepseek-ocr | hf, vllm | hf |
For detailed backend configuration, see settings/models.yaml.
# Process a single PDF (uses default: paddleocr-doclayout-v2 detector + paddleocr-vl recognizer, balanced DPI)
python main.py --input document.pdf
# Use different recognizer
python main.py --input document.pdf --recognizer deepseek-ocr
python main.py --input document.pdf --recognizer gemini-2.5-flash
python main.py --input document.pdf --recognizer gpt-4o
# Process a directory of PDFs
python main.py --input /path/to/pdfs/
# Process a single image
python main.py --input image.jpg
# Specify custom output directory
python main.py --input document.pdf --output /custom/output/
# Disable caching for fresh processing
python main.py --input document.pdf --no-cache
# Use custom prompts directory (overrides auto-detection)
python main.py --input document.pdf --prompts-dir custom_prompts/
# Backend and model combinations
python main.py --input document.pdf --backend openai --model gpt-4o-mini
python main.py --input document.pdf --backend gemini --model gemini-2.5-flash
# Page limiting options (mutually exclusive)
python main.py --input document.pdf --max-pages 5
python main.py --input document.pdf --page-range 10-20
python main.py --input document.pdf --pages 1,3,5,10,15
# Adjust detection confidence threshold
python main.py --input document.pdf --confidence 0.7
# Use custom model path
python main.py --input document.pdf --model-path /path/to/custom/model.pt
# DPI configuration for PDF-to-image conversion
python main.py --input document.pdf --dpi fast # 150 DPI - fastest
python main.py --input document.pdf --dpi balanced # 150β300 dual (recommended)
python main.py --input document.pdf --dpi quality # 300 DPI - best quality
python main.py --input document.pdf --dpi 200 # Custom single DPI
python main.py --input document.pdf --dpi 150,300 # Custom dual DPI (detection,recognition)
# Enable debug logging
python main.py --input document.pdf --log-level DEBUG
# Text correction options (disabled by default)
python main.py --input document.pdf --block-correction # Enable block-level VLM correction
python main.py --input document.pdf --page-correction # Enable page-level VLM correction
python main.py --input document.pdf --block-correction --page-correction # Enable both
# Combined advanced usage
python main.py --input /docs/ --max-pages 3 --confidence 0.8 --dpi 250
# Use different detector + sorter combinations
python main.py --input document.pdf --detector doclayout-yolo --sorter mineru-xycut
# Use PaddleOCR PP-DocLayoutV2 detector with XY-Cut sorter
python main.py --input document.pdf --detector paddleocr-doclayout-v2 --sorter mineru-xycut
# Use MinerU VLM for both detection and ordering (tightly coupled)
python main.py --input document.pdf --detector mineru-vlm --sorter mineru-vlm \
--mineru-model opendatalab/PDF-Extract-Kit-1.0
# Use multi-column aware PyMuPDF sorter
python main.py --input document.pdf --detector doclayout-yolo --sorter pymupdf
The pipeline automatically saves progress after processing each page. If processing is interrupted (error, Ctrl+C, rate limit), simply re-run the same command and it will automatically resume from where it left off.
# First run (fails at page 5 due to error or interruption)
python main.py --input document.pdf --output results/
# Creates: results/_progress.json (progress tracker)
# results/stage6_output_page1.json, page2.json, ... (checkpoints)
# Re-run (automatically resumes from page 5)
python main.py --input document.pdf --output results/
# Reads _progress.json β skips pages 1-4 β continues from page 5
# The system automatically:
# - Detects existing checkpoints in output directory
# - Validates input file matches
# - Skips already-processed pages
# - Displays resume information (last run time, completed stages, etc.)
How it works:
_progress.json tracks pipeline execution state (completed pages, timestamps, errors)--output directory# Use PaddleOCR PP-DocLayoutV2 detector + PaddleOCR-VL-0.9B recognizer
# This provides a complete PaddleOCR-based pipeline with 109 language support
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--sorter mineru-xycut
# With custom backend (vLLM or SGLang for acceleration)
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--paddleocr-vl-backend vllm-server \
--sorter mineru-xycut
# With custom query templates for specific block types
python main.py --input document.pdf \
--detector paddleocr-doclayout-v2 \
--recognizer paddleocr-vl \
--paddleocr-vl-query-table "Extract this table in markdown:" \
--sorter mineru-xycut
Available Detectors:
doclayout-yolo: This project's DocLayout-YOLO (default)paddleocr-doclayout-v2: PaddleOCR PP-DocLayoutV2 (25 categories, requires PaddleOCR v3.3.0)mineru-doclayout-yolo: MinerU's DocLayout-YOLO implementationmineru-vlm: MinerU VLM-based detectionAvailable Recognizers:
openai: OpenAI VLM backend (default, uses API)gemini: Gemini VLM backend (uses API)paddleocr-vl: PaddleOCR-VL-0.9B local model (109 languages, requires PaddleX v3.3.1)Available Sorters:
mineru-xycut: Fast XY-Cut algorithm (default, recommended)pymupdf: Multi-column aware sortingmineru-layoutreader: LayoutLMv3-based reading ordermineru-vlm: MinerU VLM-based ordering (requires mineru-vlm detector)olmocr-vlm: olmOCR VLM-based ordering
### Python API Usage
```python
from pipeline import Pipeline
# Initialize pipeline with default settings (Gemini API)
pipeline = Pipeline(
confidence_threshold=0.5,
use_cache=True,
cache_dir=".cache",
output_dir="output"
)
# Process a single image
result = pipeline.process_image("document.jpg")
print(f"Extracted text: {result['corrected_text']}")
# Process a PDF
result = pipeline.process_pdf("document.pdf")
print(f"Processed {result['num_pages']} pages")
# Process PDF with page limits
result = pipeline.process_pdf(
"document.pdf",
max_pages=5 # Process only first 5 pages
)
result = pipeline.process_pdf(
"document.pdf",
page_range=(10, 20) # Process pages 10-20
)
result = pipeline.process_pdf(
"document.pdf",
specific_pages=[1, 5, 10, 15] # Process specific pages
)
# Process a directory
result = pipeline.process_directory("input_folder/")
print(f"Processed {result['total_pdfs']} PDF files")
Each single image (or individual PDF page) is written as page_<number>.json under <output>/<model>/<document_stem>/. The payload includes:
image_path: Path to the rendered page image (or original image if supplied)width / height: Pixel dimensions of the rendered pageregions: Raw DocLayout-YOLO detections (bounding boxes and labels)processed_regions: Post-processed regions with extracted text, table summaries, etc.raw_text: Natural reading-order text composed from text-like regionscorrected_text: Text after VLM correction (falls back to raw_text on failure)correction_confidence: Similarity score between raw and corrected text (0β1)processing_time_seconds: Total latency spent on the pageprocessed_at: ISO-8601 timestamp for when processing completed{
"image_path": "output/tmp/document_page_1.jpg",
"width": 1920,
"height": 1080,
"blocks": [...],
"processed_regions": [...],
"raw_text": "Original OCR text...",
"corrected_text": "AI-corrected text...",
"correction_confidence": 0.95,
"processing_time_seconds": 12.34,
"processed_at": "2024-12-19T10:30:00"
}
PDF runs emit a summary file alongside the page outputs: summary.json (all pages succeeded), summary_partial.json (some failures), or summary_incomplete.json (stopped early). The schema captures:
pdf_name / pdf_path: Original filename and absolute pathnum_pages: Number of pages in the source PDFprocessed_pages: Count of pages processed (including fallbacks)output_directory: Folder that contains per-page and summary JSON artifactsprocessed_at: ISO-8601 timestamp for completionstatus_summary: Totals of complete, partial, and incomplete pagespages: Array of page status objects with optional file suffix (e.g., partial β page_2_partial.json)processing_stopped: Indicates an early stop due to rate limits or unexpected errors{
"pdf_name": "document",
"pdf_path": "/path/to/document.pdf",
"num_pages": 10,
"processed_pages": 10,
"output_directory": "output/gemini-2.5-flash/document",
"processed_at": "2024-12-19T10:30:00",
"status_summary": {"complete": 10},
"pages": [
{"page": 1, "status": "complete", "file_suffix": ""},
{"page": 2, "status": "partial", "file_suffix": "partial"}
],
"processing_stopped": false
}
Convert JSON output to Markdown format using two conversion strategies:
__init__.py)Simple and fast conversion using pre-classified block types:
from pipeline.conversion.output.markdown import json_to_markdown
regions = [
{"type": "title", "text": "Document Title"},
{"type": "subtitle", "text": "Section 1"},
{"type": "text", "text": "Content here."},
]
md = json_to_markdown(regions)
# # Document Title
#
# ## Section 1
#
# Content here.
Features:
pymupdf4llm.py)Advanced conversion using font size information from PDF text spans (PyMuPDF parser):
import json
from pathlib import Path
from pipeline.conversion.output.markdown.pymupdf4llm import to_markdown
# Load page result with auxiliary_info
with open("output/model/document/page_1.json") as f:
page_result = json.load(f)
# page_result contains:
# {
# "processed_regions": [...],
# "auxiliary_info": {
# "text_spans": [ # PDF text objects with font info
# {"bbox": [100, 50, 300, 80], "text": "Chapter 1", "size": 24.0, "font": "Times-Bold"}
# ]
# }
# }
# Auto-detect headers from font sizes and convert
md = to_markdown(page_result, auto_detect_headers=True)
# # Chapter 1 β 24pt β H1 (largest)
#
# ## Section 1.1 β 18pt β H2 (2nd largest)
#
# Body text. β 12pt β body text
Key Concepts:
auxiliary_info.text_spanssize and font (not font_size, font_name)How It Works:
1. PDF β Detector β Blocks (layout detection from image)
2. PDF β PyMuPDF Parser β Text Spans (font info from digital document)
3. Both saved separately in JSON (auxiliary_info)
4. Markdown conversion β IoU matching β Font-based headers
Comparison:
| Feature | Block Type-Based | Font Size-Based |
|---|---|---|
| Speed | β‘ Fast | π Slower (PDF parsing) |
| Accuracy | Layout detection dependent | Font size dependent |
| Dependencies | None | PyMuPDF (fitz) |
| Data Source | Block classification | PDF text spans |
| Use Case | Default, quick conversion | Precise header detection |
All text extraction is performed by the configured VLM backend (Gemini by default, or OpenAI/OpenRouter if selected). The model receives both rendered page images and prompt instructions tailored to the backend. Rate limiting and caching ensure the pipeline stays within API quotas while avoiding repeated work on identical regions.
For testing purposes, cost control, or processing specific sections, you can limit which pages to process:
--max-pages)Process only the first N pages from the beginning:
python main.py --input document.pdf --max-pages 5
--page-range)Process a specific range of pages:
python main.py --input document.pdf --page-range 10-20
python main.py --input document.pdf --page-range 1-5
--pages)Process only specified pages (comma-separated):
python main.py --input document.pdf --pages 1,5,10,15
python main.py --input document.pdf --pages 3,7,12
--max-pages 1 to test with just the first page before processing entire documents--max-pages 1 to test pipeline with single page--max-pages 10 to limit API calls for large documents--page-range 5-15 to process only content pages--pages 1,2 to process only first few pages--pages 10,25,50 to process sample pagesPrompts are organized by model family for optimal results. The system automatically selects the appropriate prompt directory based on the backend and model:
settings/prompts/
βββ gemini/ # Gemini-specific prompts
β βββ text_extraction.yaml
β βββ content_analysis.yaml
β βββ text_correction.yaml
βββ openai/ # OpenAI/GPT-specific prompts
β βββ text_extraction.yaml
β βββ content_analysis.yaml
β βββ text_correction.yaml
βββ internvl/ # InternVL-specific prompts
βββ qwen/ # Qwen-specific prompts
βββ phi4/ # Phi-4-specific prompts
The system automatically detects the appropriate prompt directory:
--backend gemini β settings/prompts/gemini/--backend openai --model gpt-4o β settings/prompts/openai/--backend openai --model google/gemini-2.5-flash β settings/prompts/gemini/--model internvl/internvl2-5 β settings/prompts/internvl/cp -r settings/prompts/gemini custom_promptspython main.py --input doc.pdf --prompts-dir custom_prompts/# Example: settings/prompts/text_extraction.yaml
text_extraction:
system: |
You are an expert OCR system...
user: |
Please extract all text from this image...
fallback: |
Extract all visible text accurately...
Tables are automatically detected and processed with structured analysis:
[TableStart]
## Table Structure:
| Column1 | Column2 | Column3 |
|---------|---------|---------|
| Data1 | Data2 | Data3 |
## Summary:
Brief description of table content
## Educational Significance:
Importance and context
## Related Topics:
Topic1, Topic2, Topic3
[TableEnd]
Figures and images receive detailed analysis:
[FigureStart]
## Image Description:
Detailed description of visual content
## Educational Significance:
Educational importance
## Related Topics:
Related learning topics
## Exam Relevance:
How this could be used in exams
[FigureEnd]
--cache option (default) to avoid reprocessingThe OCR Pipeline automatically creates detailed log files with timestamps for tracking and debugging.
.logs/ (hidden directory, auto-created)YYYY-MM-DD_HH-MM-SS_ocr_pipeline.log.logs/
βββ 2025-07-29_18-06-53_ocr_pipeline.log # Rate limit status check
βββ 2025-07-29_18-07-23_ocr_pipeline.log # Full OCR processing run
βββ 2025-07-29_19-15-42_ocr_pipeline.log # Another processing session
--log-level DEBUG)# Standard logging (INFO level)
python main.py --input document.pdf
# Detailed debugging logs
python main.py --input document.pdf --log-level DEBUG
# Minimal logging (ERROR only)
python main.py --input document.pdf --log-level ERROR
Log files are automatically organized by timestamp but not automatically deleted. You can:
# View recent logs
ls -la .logs/
# Remove old logs (older than 7 days)
find .logs/ -name "*.log" -mtime +7 -delete
# Archive logs by month
mkdir -p .logs/archive/2025-07/
mv .logs/2025-07-* .logs/archive/2025-07/
# Ensure you're in the project root directory
cd gemini_ocr
python main.py --input document.pdf
# Verify environment variable
echo $GEMINI_API_KEY
# Or check .env file
cat .env
# Check PyTorch CUDA installation
python -c "import torch; print(torch.cuda.is_available())"
# CPU-only operation is supported if GPU unavailable
The pipeline follows a modular design:
# In ocr_pipeline.py
def _get_gemini_prompt_for_region_type(self, region_type: str) -> str:
if region_type == 'new_type':
return "Custom prompt for new block type..."
# ... existing code
# Inherit from Pipeline
class CustomPipeline(Pipeline):
def _correct_text_with_gemini(self, text: str) -> Dict[str, Any]:
# Custom correction logic
return super()._correct_text_with_gemini(text)
This project is for educational and research purposes. Please ensure compliance with the respective API provider's terms of service and usage limits.
For issues and questions:
GEMINI_API_KEY, OPENAI_API_KEY).logs/Note: This system requires internet connectivity for API calls to the selected VLM provider. All processing results are stored locally for privacy and offline access.
180 commits
Python
99.7%