PDF to Markdown OCR using vision-language models with multi-GPU support
Python
3
30 commits
updated Mar 15, 2026
A professional PDF OCR system that leverages state-of-the-art vision-language models for high-quality text extraction with built-in multi-GPU acceleration support.
.md (VLMs), .txt (GLM-OCR OCR), .json (OCRFlux full doc or Extract Info)# Install dependencies
pip install -r requirements.txt
# Set up HuggingFace token (optional)
cp env.example .env
# Edit .env and add your HF_TOKEN
# Launch both Web UI and API together (recommended)
python main.py serve
# Web UI: http://localhost:7860
# API: http://localhost:8000
# API Docs: http://localhost:8000/docs
# With public share link (for Kaggle, Colab, remote access)
python main.py serve --share
# Or launch individually
python main.py ui # Just the Web UI
python main.py ui --share # With public link
python main.py api # Just the API
# Or use CLI for direct processing
python main.py process document.pdf --model "Qwen3-VL-4B-Instruct" --output result.md
# List all available models
python main.py list-models
Before installing Python dependencies, ensure you have Poppler installed:
sudo apt-get update
sudo apt-get install -y poppler-utils
brew install poppler
!apt-get install -y poppler-utils
git clone https://github.com/yourusername/pdf-ocr-llm.git
cd pdf-ocr-llm
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
# Copy the example file
cp env.example .env
# Edit .env and add your HuggingFace token
# Get your token from: https://huggingface.co/settings/tokens
Your .env file should contain:
HF_TOKEN=your_huggingface_token_here
Note: The HuggingFace token is required if you're accessing gated models or private repositories. For public models, it's optional but recommended.
The project uses a YAML configuration file (config.yaml) to manage models and settings. You can customize:
Example configuration structure:
models:
qwen3vl:
- name: "Qwen3-VL-4B-Instruct"
model_id: "Qwen/Qwen3-VL-4B-Instruct"
type: "qwen3vl"
device:
use_multi_gpu: true
device_map: "auto"
Launch both the web interface and API server together:
python main.py serve
This starts:
http://localhost:7860 - Interactive interfacehttp://localhost:8000 - Programmatic accesshttp://localhost:8000/docs - Interactive API documentationOptions:
--api-host HOST - API server host (default: 0.0.0.0)--api-port PORT - API server port (default: 8000)--ui-host HOST - UI server host (default: 0.0.0.0)--ui-port PORT - UI server port (default: 7860)--share - Create public share link for UI (useful for remote access)The easiest way to use the application is through the web interface:
python main.py ui
Then open your browser to http://localhost:7860
Options:
--host HOST - Server host (default: 0.0.0.0)--port PORT - Server port (default: 7860)--share - Create public share linkFeatures:
.md, .txt (GLM-OCR), or .json (OCRFlux).json.md, .txt (GLM-OCR), or .json (OCRFlux, Extract Info)Run just the API server:
python main.py api
API will be available at http://localhost:8000
Options: --host HOST (default: 0.0.0.0), --port PORT (default: 8000)
| Method | Path | Description |
|---|---|---|
GET | / | API info and list of endpoints |
GET | /models | List available models (names and config) |
POST | /process/pdf | Upload a PDF, get extracted text |
POST | /process/image | Upload an image, get extracted text |
GET | /health | Health check |
POST /process/pdfmultipart/form-data
file (required): PDF filemodel_name (required): Exact model name from /models (e.g. Qwen3-VL-4B-Instruct, GLM-OCR, InternVL3.5-8B-Instruct)prompt (optional): Custom OCR prompt; omit for defaultsuccess: truemodel: model name usednum_pages: number of pagesmarkdown: extracted text (markdown for VLMs, plain text for GLM-OCR, JSON string for OCRFlux)processing_time: (if present) secondsPOST /process/imagemultipart/form-data
file (required): Image file (e.g. PNG, JPEG)model_name (required): Model name from /modelsprompt (optional): Custom promptsuccess, model, markdown, text_length# List models
curl http://localhost:8000/models
# Process a PDF
curl -X POST http://localhost:8000/process/pdf \
-F "[email protected]" \
-F "model_name=Qwen3-VL-4B-Instruct"
# With custom prompt
curl -X POST http://localhost:8000/process/pdf \
-F "[email protected]" \
-F "model_name=GLM-OCR" \
-F "prompt=Text Recognition:"
# Process an image
curl -X POST http://localhost:8000/process/image \
-F "[email protected]" \
-F "model_name=InternVL3.5-8B-Instruct"
import requests
BASE = "http://localhost:8000"
# List models
r = requests.get(f"{BASE}/models")
print(r.json()) # {"models": [{"name": "Qwen3-VL-4B-Instruct", ...}, ...]}
# Process PDF
with open("document.pdf", "rb") as f:
r = requests.post(
f"{BASE}/process/pdf",
files={"file": ("document.pdf", f, "application/pdf")},
data={"model_name": "Qwen3-VL-4B-Instruct"}
)
data = r.json()
print(data["markdown"]) # extracted text
print(data["num_pages"])
# Process image with optional prompt
with open("scan.png", "rb") as f:
r = requests.post(
f"{BASE}/process/image",
files={"file": ("scan.png", f, "image/png")},
data={
"model_name": "GLM-OCR",
"prompt": "Text Recognition:" # or "Formula Recognition:" / "Table Recognition:"
}
)
print(r.json()["markdown"])
Interactive API docs: Open http://localhost:8000/docs (Swagger UI) to try the endpoints from the browser.
View all configured models:
python main.py list-models
Extract text from a PDF file to Markdown:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --output output.md
Save intermediate images:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --save-images --images-dir ./images
Extract text from an image file to Markdown:
python main.py process image.png --model "Qwen3-VL-4B-Instruct" --output output.md
Process multiple files at once:
python main.py batch --model "Qwen3-VL-4B-Instruct" --input-dir ./pdfs --output-dir ./outputs
Process specific files:
python main.py batch --model "Qwen3-VL-4B-Instruct" --input-files file1.pdf file2.pdf --output-dir ./outputs
Use a custom prompt for better context:
python main.py process invoice.pdf --model "Qwen3-VL-4B-Instruct" --prompt "Extract all text from this invoice, preserving table structure"
Display available GPUs:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --show-device-info
pdf-ocr-llm/
├── config.yaml # Configuration file
├── main.py # Main entry point
├── requirements.txt # Python dependencies
├── README.md # This file
└── src/
├── config/
│ └── config_manager.py # Configuration management
├── models/
│ ├── base_model.py # Abstract base class
│ ├── qwen3vl_model.py # Qwen 3 VL implementation
│ ├── internvl_model.py # InternVL 3.5 Instruct implementation
│ ├── ocrflux_model.py # OCRFlux-3B (ChatDOC, markdown/JSON)
│ ├── glm_ocr_model.py # GLM-OCR (1 image per call; document + info extraction)
│ └── model_factory.py # Model factory pattern
├── processors/
│ └── pdf_processor.py # PDF processing utilities
├── pipeline/
│ └── ocr_pipeline.py # Main OCR pipeline
├── ui/
│ └── app.py # Gradio Web UI
├── api/
│ └── app.py # FastAPI REST API
└── utils/
└── device_manager.py # Device management (GPU)
The project follows a professional, modular architecture:
The project supports multiple vision-language model families for OCR and document understanding:
General-purpose vision-language models with strong OCR and markdown output:
📚 Reference: Qwen 3 VL on HuggingFace
Versatile multimodal models with dynamic image preprocessing and tiling; output is markdown:
Note: InternVL uses the GitHub/Instruct format (
trust_remote_code) and dynamic tiling for high-resolution documents.
📚 Reference: InternVL3.5-1B-Instruct
Dedicated document OCR model (0.9B); not a general-purpose VLM. Output is plain text by default. Supports:
"Text Recognition:", "Formula Recognition:", "Table Recognition:"When using GLM-OCR for standard OCR, downloads use .txt; for Extract Info, output is a JSON array (one object per image). GLM-OCR accepts one image per call; the app batches by running it once per PDF page or per uploaded image.
📚 Reference: GLM-OCR on HuggingFace
Document OCR model (3B, Qwen2.5-VL based); outputs clean markdown. The pipeline returns a JSON array (one object per page with fields such as natural_text, primary_language, is_table). Download uses .json.
📚 Reference: OCRFlux-3B on HuggingFace
Note: Qwen 2.5 VL (3B, 7B, 32B, 72B) has been removed in favor of Qwen 3 VL and the current model set. Use config.yaml to adjust which models are available.
The project automatically distributes model layers across multiple GPUs when available. Configure in config.yaml:
device:
use_multi_gpu: true
device_map: "auto" # Automatic distribution
Run locally with all features:
python main.py serve
Perfect for free GPU access with public sharing:
Kaggle Notebook:
# Clone and setup
!git clone https://github.com/yourusername/pdf-ocr-llm.git
%cd pdf-ocr-llm
!pip install -r requirements.txt
# Launch with public link
!python main.py ui --share
# Or both API + UI
!python main.py serve --share
Google Colab:
# Same as Kaggle
!git clone https://github.com/yourusername/pdf-ocr-llm.git
%cd pdf-ocr-llm
!pip install -r requirements.txt
# Launch with sharing enabled
!python main.py ui --share
The --share flag creates a temporary public URL (valid for 72 hours) that you can access from anywhere.
Using Docker Compose:
docker-compose up
Access at:
http://localhost:7860http://localhost:8000For production deployment with Nginx/Apache reverse proxy:
# Run API and UI on different ports
python main.py api --host 0.0.0.0 --port 8000
python main.py ui --host 0.0.0.0 --port 7860
# Or use serve for both
python main.py serve --api-port 8000 --ui-port 7860
PDF to Markdown OCR using vision-language models with multi-GPU support
Python
3
30 commits
updated Mar 15, 2026
A professional PDF OCR system that leverages state-of-the-art vision-language models for high-quality text extraction with built-in multi-GPU acceleration support.
.md (VLMs), .txt (GLM-OCR OCR), .json (OCRFlux full doc or Extract Info)# Install dependencies
pip install -r requirements.txt
# Set up HuggingFace token (optional)
cp env.example .env
# Edit .env and add your HF_TOKEN
# Launch both Web UI and API together (recommended)
python main.py serve
# Web UI: http://localhost:7860
# API: http://localhost:8000
# API Docs: http://localhost:8000/docs
# With public share link (for Kaggle, Colab, remote access)
python main.py serve --share
# Or launch individually
python main.py ui # Just the Web UI
python main.py ui --share # With public link
python main.py api # Just the API
# Or use CLI for direct processing
python main.py process document.pdf --model "Qwen3-VL-4B-Instruct" --output result.md
# List all available models
python main.py list-models
Before installing Python dependencies, ensure you have Poppler installed:
sudo apt-get update
sudo apt-get install -y poppler-utils
brew install poppler
!apt-get install -y poppler-utils
git clone https://github.com/yourusername/pdf-ocr-llm.git
cd pdf-ocr-llm
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
# Copy the example file
cp env.example .env
# Edit .env and add your HuggingFace token
# Get your token from: https://huggingface.co/settings/tokens
Your .env file should contain:
HF_TOKEN=your_huggingface_token_here
Note: The HuggingFace token is required if you're accessing gated models or private repositories. For public models, it's optional but recommended.
The project uses a YAML configuration file (config.yaml) to manage models and settings. You can customize:
Example configuration structure:
models:
qwen3vl:
- name: "Qwen3-VL-4B-Instruct"
model_id: "Qwen/Qwen3-VL-4B-Instruct"
type: "qwen3vl"
device:
use_multi_gpu: true
device_map: "auto"
Launch both the web interface and API server together:
python main.py serve
This starts:
http://localhost:7860 - Interactive interfacehttp://localhost:8000 - Programmatic accesshttp://localhost:8000/docs - Interactive API documentationOptions:
--api-host HOST - API server host (default: 0.0.0.0)--api-port PORT - API server port (default: 8000)--ui-host HOST - UI server host (default: 0.0.0.0)--ui-port PORT - UI server port (default: 7860)--share - Create public share link for UI (useful for remote access)The easiest way to use the application is through the web interface:
python main.py ui
Then open your browser to http://localhost:7860
Options:
--host HOST - Server host (default: 0.0.0.0)--port PORT - Server port (default: 7860)--share - Create public share linkFeatures:
.md, .txt (GLM-OCR), or .json (OCRFlux).json.md, .txt (GLM-OCR), or .json (OCRFlux, Extract Info)Run just the API server:
python main.py api
API will be available at http://localhost:8000
Options: --host HOST (default: 0.0.0.0), --port PORT (default: 8000)
| Method | Path | Description |
|---|---|---|
GET | / | API info and list of endpoints |
GET | /models | List available models (names and config) |
POST | /process/pdf | Upload a PDF, get extracted text |
POST | /process/image | Upload an image, get extracted text |
GET | /health | Health check |
POST /process/pdfmultipart/form-data
file (required): PDF filemodel_name (required): Exact model name from /models (e.g. Qwen3-VL-4B-Instruct, GLM-OCR, InternVL3.5-8B-Instruct)prompt (optional): Custom OCR prompt; omit for defaultsuccess: truemodel: model name usednum_pages: number of pagesmarkdown: extracted text (markdown for VLMs, plain text for GLM-OCR, JSON string for OCRFlux)processing_time: (if present) secondsPOST /process/imagemultipart/form-data
file (required): Image file (e.g. PNG, JPEG)model_name (required): Model name from /modelsprompt (optional): Custom promptsuccess, model, markdown, text_length# List models
curl http://localhost:8000/models
# Process a PDF
curl -X POST http://localhost:8000/process/pdf \
-F "[email protected]" \
-F "model_name=Qwen3-VL-4B-Instruct"
# With custom prompt
curl -X POST http://localhost:8000/process/pdf \
-F "[email protected]" \
-F "model_name=GLM-OCR" \
-F "prompt=Text Recognition:"
# Process an image
curl -X POST http://localhost:8000/process/image \
-F "[email protected]" \
-F "model_name=InternVL3.5-8B-Instruct"
import requests
BASE = "http://localhost:8000"
# List models
r = requests.get(f"{BASE}/models")
print(r.json()) # {"models": [{"name": "Qwen3-VL-4B-Instruct", ...}, ...]}
# Process PDF
with open("document.pdf", "rb") as f:
r = requests.post(
f"{BASE}/process/pdf",
files={"file": ("document.pdf", f, "application/pdf")},
data={"model_name": "Qwen3-VL-4B-Instruct"}
)
data = r.json()
print(data["markdown"]) # extracted text
print(data["num_pages"])
# Process image with optional prompt
with open("scan.png", "rb") as f:
r = requests.post(
f"{BASE}/process/image",
files={"file": ("scan.png", f, "image/png")},
data={
"model_name": "GLM-OCR",
"prompt": "Text Recognition:" # or "Formula Recognition:" / "Table Recognition:"
}
)
print(r.json()["markdown"])
Interactive API docs: Open http://localhost:8000/docs (Swagger UI) to try the endpoints from the browser.
View all configured models:
python main.py list-models
Extract text from a PDF file to Markdown:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --output output.md
Save intermediate images:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --save-images --images-dir ./images
Extract text from an image file to Markdown:
python main.py process image.png --model "Qwen3-VL-4B-Instruct" --output output.md
Process multiple files at once:
python main.py batch --model "Qwen3-VL-4B-Instruct" --input-dir ./pdfs --output-dir ./outputs
Process specific files:
python main.py batch --model "Qwen3-VL-4B-Instruct" --input-files file1.pdf file2.pdf --output-dir ./outputs
Use a custom prompt for better context:
python main.py process invoice.pdf --model "Qwen3-VL-4B-Instruct" --prompt "Extract all text from this invoice, preserving table structure"
Display available GPUs:
python main.py process input.pdf --model "Qwen3-VL-4B-Instruct" --show-device-info
pdf-ocr-llm/
├── config.yaml # Configuration file
├── main.py # Main entry point
├── requirements.txt # Python dependencies
├── README.md # This file
└── src/
├── config/
│ └── config_manager.py # Configuration management
├── models/
│ ├── base_model.py # Abstract base class
│ ├── qwen3vl_model.py # Qwen 3 VL implementation
│ ├── internvl_model.py # InternVL 3.5 Instruct implementation
│ ├── ocrflux_model.py # OCRFlux-3B (ChatDOC, markdown/JSON)
│ ├── glm_ocr_model.py # GLM-OCR (1 image per call; document + info extraction)
│ └── model_factory.py # Model factory pattern
├── processors/
│ └── pdf_processor.py # PDF processing utilities
├── pipeline/
│ └── ocr_pipeline.py # Main OCR pipeline
├── ui/
│ └── app.py # Gradio Web UI
├── api/
│ └── app.py # FastAPI REST API
└── utils/
└── device_manager.py # Device management (GPU)
The project follows a professional, modular architecture:
The project supports multiple vision-language model families for OCR and document understanding:
General-purpose vision-language models with strong OCR and markdown output:
📚 Reference: Qwen 3 VL on HuggingFace
Versatile multimodal models with dynamic image preprocessing and tiling; output is markdown:
Note: InternVL uses the GitHub/Instruct format (
trust_remote_code) and dynamic tiling for high-resolution documents.
📚 Reference: InternVL3.5-1B-Instruct
Dedicated document OCR model (0.9B); not a general-purpose VLM. Output is plain text by default. Supports:
"Text Recognition:", "Formula Recognition:", "Table Recognition:"When using GLM-OCR for standard OCR, downloads use .txt; for Extract Info, output is a JSON array (one object per image). GLM-OCR accepts one image per call; the app batches by running it once per PDF page or per uploaded image.
📚 Reference: GLM-OCR on HuggingFace
Document OCR model (3B, Qwen2.5-VL based); outputs clean markdown. The pipeline returns a JSON array (one object per page with fields such as natural_text, primary_language, is_table). Download uses .json.
📚 Reference: OCRFlux-3B on HuggingFace
Note: Qwen 2.5 VL (3B, 7B, 32B, 72B) has been removed in favor of Qwen 3 VL and the current model set. Use config.yaml to adjust which models are available.
The project automatically distributes model layers across multiple GPUs when available. Configure in config.yaml:
device:
use_multi_gpu: true
device_map: "auto" # Automatic distribution
Run locally with all features:
python main.py serve
Perfect for free GPU access with public sharing:
Kaggle Notebook:
# Clone and setup
!git clone https://github.com/yourusername/pdf-ocr-llm.git
%cd pdf-ocr-llm
!pip install -r requirements.txt
# Launch with public link
!python main.py ui --share
# Or both API + UI
!python main.py serve --share
Google Colab:
# Same as Kaggle
!git clone https://github.com/yourusername/pdf-ocr-llm.git
%cd pdf-ocr-llm
!pip install -r requirements.txt
# Launch with sharing enabled
!python main.py ui --share
The --share flag creates a temporary public URL (valid for 72 hours) that you can access from anywhere.
Using Docker Compose:
docker-compose up
Access at:
http://localhost:7860http://localhost:8000For production deployment with Nginx/Apache reverse proxy:
# Run API and UI on different ports
python main.py api --host 0.0.0.0 --port 8000
python main.py ui --host 0.0.0.0 --port 7860
# Or use serve for both
python main.py serve --api-port 8000 --ui-port 7860