PDF to markdown using vision LLMs — tables, layouts, and structure preserved
LLM-powered PDF to markdown. uses vision models to actually read your documents — tables, headers, mixed layouts — and outputs clean, structured markdown. not traditional OCR.
curl -X POST "http://localhost:8000/ocr" -F "file=@document.pdf"
demo
https://github.com/user-attachments/assets/6b39f3ea-248e-4c29-ac2e-b57de64d5d65
NASA Apollo 17 flight docs — mixed orientations, messy layouts — converted to structured markdown.
what it does
- vision model OCR — understands context, not just character shapes
- parallel processing — 50-page PDF in seconds, not minutes
- table preservation — detected and formatted as proper markdown tables
- smart batching — configurable pages-per-request for speed vs accuracy tradeoff
- retry with backoff — handles rate limits and timeouts without crashing
- flexible input — file upload or URL, your choice
- image descriptions — non-text elements get
[Image: description]annotations
cost
using OpenAI as an example (~1,500 tokens/page average):
| model | cost per 1,000 pages | |:---|:---| | GPT-4o | ~$15 | | GPT-4o mini | ~$8 | | batch API | ~$4 |
works with any OpenAI-compatible vision API. swap the endpoint and model in config.
install
git clone https://github.com/yigitkonur/api-llm-ocr.git
cd api-llm-ocr
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt
configure
create a .env file:
# required
OPENAIAPIKEY=yourapikey
AZUREOPENAIENDPOINT=https://your-resource.openai.azure.com/
OPENAIDEPLOYMENTID=yourvisionmodel_deployment
optional
OPENAIAPIVERSION=gpt-4o
BATCH_SIZE=1
MAXCONCURRENTOCR_REQUESTS=5
MAXCONCURRENTPDF_CONVERSION=4
run
# pick one
uvicorn main:app --reload
uvicorn swift_ocr.app:app --reload
python -m swift_ocr
python -m swift_ocr --host 0.0.0.0 --port 8080 --workers 4
API lives at http://127.0.0.1:8000. auto-generated docs at /docs.
usage
upload a file
curl -X POST "http://127.0.0.1:8000/ocr" \
-F "file=@/path/to/document.pdf"
process from URL
curl -X POST "http://127.0.0.1:8000/ocr" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/document.pdf"}'
response
{
"text": "# document title\n\n## section 1\n\nextracted text...",
"status": "success",
"pages_processed": 5,
"processingtimems": 1234
}
health check
curl http://127.0.0.1:8000/health
error codes
| code | meaning | |:---|:---| | 200 | success | | 400 | bad request (no file/URL, or both provided) | | 422 | validation error | | 429 | rate limited — retry with backoff | | 500 | processing error | | 504 | timeout downloading PDF |
configuration
| variable | default | description | |:---|:---|:---| | OPENAIAPIKEY | — | API key | | AZUREOPENAIENDPOINT | — | endpoint URL | | OPENAIDEPLOYMENTID | — | vision model deployment ID | | OPENAIAPIVERSION | gpt-4o | API version | | BATCH_SIZE | 1 | pages per OCR request (1-10). higher = faster, less accurate | | MAXCONCURRENTOCR_REQUESTS | 5 | parallel OCR calls | | MAXCONCURRENTPDF_CONVERSION | 4 | parallel page renders. match your CPU cores |
tuning
- high accuracy:
BATCH_SIZE=1 - balanced:
BATCHSIZE=5,MAXCONCURRENTOCRREQUESTS=10 - max throughput:
BATCHSIZE=10,MAXCONCURRENTOCRREQUESTS=20(watch rate limits)
project structure
swift_ocr/
init.py — package init
main.py — CLI entry point
app.py — FastAPI app factory
config/
settings.py — pydantic settings (type-safe config)
core/
exceptions.py — custom exception hierarchy
logging.py — structured logging
retry.py — exponential backoff
schemas/
ocr.py — pydantic request/response models
services/
ocr.py — vision model OCR service
pdf.py — PDF conversion service
api/
deps.py — dependency injection
exceptions.py — FastAPI exception handlers
router.py — route aggregation
routes/
health.py — health check endpoints
ocr.py — OCR endpoints
troubleshooting
| problem | fix | |:---|:---| | missing env vars | check .env has OPENAIAPIKEY, AZUREOPENAIENDPOINT, OPENAIDEPLOYMENTID | | 429 rate limits | reduce MAXCONCURRENTOCRREQUESTS or BATCHSIZE | | timeout errors | large PDFs take time — backoff is built in | | garbled output | make sure your PDF isn't password-protected or corrupted | | tables misformatted | try BATCH_SIZE=1 for complex tables | | failed to init client | verify endpoint format: https://your-resource.openai.azure.com/ |
license
AGPL v3 — required by PyMuPDF dependency.
if you want MIT, swap PyMuPDF for pdf2image + Poppler. the rest of the code is yours.