Documentation

TextLens docs

Everything you need to read text from images and documents with TextLens 2.0. Each section links to the full guide on GitHub.

Installation#

TextLens needs Python 3.10 or newer. The core install reads PDFs natively and runs OCR on the CPU — no PyTorch, no CUDA, no OpenCV.

bash
pip install textlens-ocr
textlens doctor        # check hardware, runtimes and models
The package is textlens-ocr, the import is textlens. pip install textlens installs an unrelated project.

Optional extras

ExtraAddsInstall
gpuDocument VLMs (GLM-OCR, LightOnOCR, HunyuanOCR, SmolVLM) via PyTorchpip install "textlens-ocr[gpu]"
serverREST API (textlens serve)pip install "textlens-ocr[server]"
documentsDOCX and PPTX inputpip install "textlens-ocr[documents]"
anprNumber-plate detection and recognitionpip install "textlens-ocr[anpr]"
mcpMCP server for AI agentspip install "textlens-ocr[mcp]"
uiRich terminal output, system telemetrypip install "textlens-ocr[ui]"
vllmvLLM for self-hosting large models (Linux)pip install "textlens-ocr[vllm]"
allEverything above except vllmpip install "textlens-ocr[all]"

Combine extras with a comma: pip install "textlens-ocr[server,documents]". TextLens never runs pip install for you — when a feature needs a package, the error message tells you the exact command.

Full installation guide →

Quickstart#

Create an OCR object once and call it on any file. The first call downloads the default model (about 31 MB) and verifies its SHA-256.

python
from textlens import OCR

ocr = OCR()
result = ocr("invoice.pdf")      # path, URL, bytes, file object, PIL image or numpy array

print(result.text)
print(result.to_markdown())
result.save("output/")             # .json, .md, .txt and tables as .csv

The same from the command line:

bash
textlens ocr invoice.pdf
textlens ocr invoice.pdf -f markdown -o invoice.md
textlens setup           # optional: detect hardware, choose a profile, install models
Quickstart guide →

Configuration#

Settings come from arguments first, then environment variables, then the config file written by textlens setup.

VariableDefaultMeaning
TEXTLENS_HOME~/.cache/textlensBase folder for models, cache and config
TEXTLENS_MODELS_DIR$TEXTLENS_HOME/modelsWhere model weights are stored
TEXTLENS_PROFILEautoDefault routing profile
TEXTLENS_DEVICEautocpu, cuda, cuda:N, tensorrt, coreml, dml
TEXTLENS_OFFLINE01 turns every download into an error with instructions
TEXTLENS_RESULT_CACHEmemoryResult cache: off, memory or disk
TEXTLENS_NO_PERSIST01 never writes results to disk
TEXTLENS_MAX_PAGES / _MAX_FILE_MB—Input limits
TEXTLENS_ALLOW_URLS1Accept http(s) inputs in Python and the CLI (the server has its own switch)
TEXTLENS_LOG_LEVEL / _LOG_FORMATWARNING / textjson for structured logs
TEXTLENS_OPENAI_BASE_URL—Inference server for served models (vLLM…)

Server-only settings (API keys, workers, rate limits) are listed under REST API.

Troubleshooting#

Start with textlens doctor (add --deep to also check that CUDA works in PyTorch). Every error has a code and a Hint line with the next step.

ProblemFix
No matching distribution for textlensThe package is textlens-ocr.
onnxruntime fails on a Raspberry PiUse a 64-bit OS; there are no 32-bit ARM wheels.
A scanned PDF returns empty textRun textlens inspect file.pdf; force OCR with OCR(ocr="force") or --ocr force.
GPU present but runs on CPUYour PyTorch is a CPU build. textlens doctor prints the CUDA install command.
PP-OCR should use the GPUpip uninstall -y onnxruntime && pip install onnxruntime-gpu — never both.
CUDA out of memoryUse a smaller profile or OCR(memory_budget_gb=4).
Low-confidence pagesresult.pages_needing_review() lists them; add a fallback model.
All troubleshooting tips →

Results & exports#

Every call returns a Result with the same shape, whichever model produced it: pages → blocks → lines → words, plus tables and formulas. Every element has a bounding box (top-left origin; PDF pages in points) and a confidence score.

python
result = OCR()("report.pdf")

result.text, result.confidence, result.page_count
for page in result.pages:
    print(page.number, page.provenance.source, page.provenance.model, page.provenance.reasons)
    for block in page.ordered_blocks():
        print(block.type, block.bbox, block.text[:60])

for table in result.tables:
    table.to_csv(), table.to_markdown(), table.to_records()

result.to_markdown(page_markers=True)
result.to_html()
result.to_json()                   # Result.from_json() reads it back
result.save("output/")
The result schema →

PDFs & selective OCR#

Before anything is rendered, TextLens inspects each PDF page. Pages with a good text layer are extracted directly — exact and fast. Only pages that need it are OCR'd, and each decision has a reason code.

ReasonMeaning
scannedThe page is an image with no usable text
no_textNo text layer at all
vector_textText drawn as paths (outlined fonts)
invisible_text_layerA scanner's hidden OCR layer over an image
suspected_garbled_textBroken font encoding — the text would be gibberish
sparse_text_over_imageA few words over a mostly-image page
python
import textlens
from textlens import OCR

ocr = OCR()
print(ocr.inspect("report.pdf").summary())    # no rendering, no OCR

ocr("report.pdf", pages="1-3")                  # a page range
OCR(ocr="force")("report.pdf")                # OCR every page
OCR(ocr="off")("report.pdf")                  # native text only

for page in ocr.stream("big.pdf"):             # constant memory
    print(page.number, len(page.text))

doc = textlens.load("big.pdf")                   # lazy: only touched pages run
doc.page(12).text
bash
textlens inspect report.pdf
textlens inspect report.pdf --overlay out/   # draw blocks in reading order
textlens ocr report.pdf --ocr force
PDF guide →

Profiles & routing#

You normally never name a model. For each page that needs OCR, the router chooses a primary model and a fallback chain from what is installed and what your hardware can run.

ProfileUse it whenBehaviour
autoDefaultPicks one of the profiles below from your hardware and installed models
edgeRaspberry Pi, Jetson, low RAMONNX PP-OCR only, 150 dpi, small detector input
fastCPU laptops, high volumePP-OCR on CPU or GPU, 200 dpi
balancedA GPU with a VLM installedPP-OCR for simple pages, a VLM for tables, formulas and hard pages
accurateQuality firstBest installed model first, lighter ones as fallback
documentPDF → structured MarkdownGenerative document models first
serverAn inference server is configuredPrefers the remote endpoint, high concurrency
python
OCR(profile="balanced")
OCR(quality="high")                        # shorthand for accurate under auto
OCR(latency_budget_ms=500, memory_budget_gb=4)
OCR(commercial_only=True)                 # skip models with restrictive licenses
OCR(fallback_models=["glm-ocr"], min_confidence=0.85)

for decision in OCR().explain("scan.pdf"):  # dry run: why each model was chosen
    print(decision.explain())
How routing works →

Models#

A small, curated catalog covers different hardware and tasks. Each model has a card with its capabilities, hardware needs, license and known limitations.

bash
textlens models list                 # catalog + install status
textlens models search table
textlens models info glm-ocr         # model card: hardware, license, limitations
textlens models install glm-ocr
textlens models verify ppocrv6-small # re-check SHA-256
textlens models remove glm-ocr
ModelRuns onTasksLicense
ppocrv6-small defaultAny CPU, Pi, JetsonText lines, boxes, confidenceApache-2.0
glm-ocrGPU ≥ 4 GBText, tables, formulas, MarkdownMIT
lighton-ocrGPU ≥ 6 GBText, tables, formulas, MarkdownApache-2.0
hunyuan-ocrGPU ≥ 6 GBLayouts, charts, extractionTencent community (restricted)
smolvlmCPU or small GPURough textApache-2.0
paddleocr-vl servedGPU server (vLLM…)Text, tables, formulas, charts; 100+ languagesApache-2.0
deepseek-ocr-2 servedGPU server ≥ 10 GBDense long documentsApache-2.0
deepseek-ocr servedGPU server ≥ 10 GBDocumentsMIT

Served models run in an inference server such as vLLM or SGLang. Point TextLens at it:

python
OCR(model="paddleocr-vl", endpoint="http://gpu-box:8000/v1")
Check licenses before commercial use. Model weights are downloaded separately under their own licenses; textlens models info <name> prints the license and notes.
Model catalog →

Field extraction#

Describe the fields you want; TextLens returns typed values with the page and box each was found at. It uses local heuristics by default and a VLM when one is installed.

python
schema = {"vendor": "string", "invoice_number": "string", "date": "date", "total": "float"}
ext = OCR().extract("invoice.pdf", schema=schema)

ext.data                 # {"vendor": ..., "date": ..., "total": ...}
ext.method               # how the values were found
ext.fields["total"]      # found, page, bbox
bash
textlens extract invoice.pdf -s "invoice_number,date,total"
Extraction guide →

RAG chunks#

Turn any document into section-aware chunks with page numbers and bounding boxes, ready for a vector store and for showing citations.

python
result = OCR()("handbook.pdf")
chunks = result.to_chunks(max_tokens=400, overlap_tokens=40, strategy="semantic")

records = [
    {
        "text": c.text,
        "metadata": {"pages": c.pages, "section": " > ".join(c.section), "boxes": c.bboxes},
    }
    for c in chunks
]
RAG guide →

Batch processing#

Process folders, globs or lists. One model copy is shared across files; errors are returned per file instead of stopping the run.

python
results = OCR().batch("./documents", workers=4)
for r in results:
    if isinstance(r, Exception):
        print("failed:", r)
    else:
        print(r.metadata["source_name"], r.page_count)
bash
textlens batch ./documents -o results/                  # opens a live dashboard
textlens batch ./documents -o results/ --no-dashboard

The dashboard shows jobs, logs and CPU/RAM/GPU telemetry, lets you pause, resume and retry, and exports a PDF report.

Number plates (ANPR)#

An optional pipeline for vehicle number plates: detection, recognition, region format validation and, for video, tracking with voting across frames.

python
# pip install "textlens-ocr[anpr]"
from textlens import ANPR

anpr = ANPR(profile="balanced", region="in")
for plate in anpr("car.jpg").plates:
    print(plate.plate, plate.confidence, plate.valid)

Regions: generic, in, uk, de, fr, it, es, nl, eu, us, br, ae, or your own JSON/YAML format file.

ANPR guide →

CLI#

Run textlens <command> --help for all options.

CommandWhat it does
textlens ocr FILE…OCR images and documents (selective OCR, routed models). -f text|markdown|html|json, -o, --pages, --ocr, --profile, --model
textlens document FILEStructure-first conversion to Markdown, JSON or HTML
textlens inspect FILEWhich pages need OCR and why, without OCR. --overlay DIR
textlens extract FILE -s …Extract fields to JSON
textlens batch DIR -o OUTProcess a folder, with an optional live dashboard
textlens models …list, search, info, install, remove, verify, path
textlens doctorDiagnose hardware, runtimes and model fit. --deep, --json
textlens setupGuided setup: detect hardware, choose a profile, install models
textlens serveRun the REST API server
textlens mcpRun the MCP server for AI agents
textlens benchmarkCompare models on your own labelled data (CER, WER, latency, memory)
textlens profile FILEPer-stage timings, model load time, peak RAM/VRAM
textlens anpr FILENumber-plate recognition
textlens cacheResult cache stats or clear

Python API#

Everything public is importable from textlens. Importing it loads no ML runtime — models load on first use.

CallReturns
ocr(source, **overrides)Result. Overrides: pages, ocr, task, prompt, cache…
ocr.document(source)Result using the Markdown task
ocr.stream(source)Iterator of Page, constant memory
ocr.batch(sources, workers=None)List of Result or exception per input
ocr.inspect(source)Page classification for PDFs, signals for images
ocr.explain(source)Routing decisions, no OCR
ocr.extract(source, schema)Extraction(data, fields, method, result)
ocr.read(source)str — the 0.x API
ocr.warmup() / ocr.unload()Load or free models
textlens.load(source)Lazy Document
textlens.inspect_system()Hardware and runtime report

Common OCR(...) arguments: model, profile, device, ocr, dpi, task, endpoint, fallback_models, min_confidence, latency_budget_ms, memory_budget_gb, commercial_only, cache, password.

Full API reference →

REST API#

bash
pip install "textlens-ocr[server]"
textlens serve                                            # http://127.0.0.1:8000, docs at /docs
TEXTLENS_API_KEYS=k1,k2 textlens serve --host 0.0.0.0 --workers 2 --warmup

curl -H "X-API-Key: k1" -F file=@scan.pdf localhost:8000/ocr
curl -H "X-API-Key: k1" -F file=@scan.pdf -F format=markdown -F pages=1-3 localhost:8000/ocr
curl -H "X-API-Key: k1" -F file=@invoice.pdf -F 'schema={"total":"float","date":"date"}' localhost:8000/extract
curl -H "X-API-Key: k1" -F file=@big.pdf -F async=true localhost:8000/ocr     # → {"job_id": "…"}
EndpointPurpose
POST /ocrImage, PDF, DOCX or PPTX → result (format=json|text|markdown|html)
POST /documentStructure-first processing (Markdown by default)
POST /extractschema → JSON fields
POST /inspectPDF classification, no OCR
POST /batchMany files → async job
GET /jobs/{id}, /jobs/{id}/resultJob status and result; DELETE cancels
GET /models, /systemCatalog and hardware status
GET /health, /ready, /metricsLiveness, readiness, Prometheus metrics
POST /v1/chat/completionsOpenAI-compatible facade

Key settings: TEXTLENS_API_KEYS (auth is off when empty), TEXTLENS_WORKERS, TEXTLENS_MAX_QUEUE, TEXTLENS_MAX_UPLOAD_MB, TEXTLENS_RATE_LIMIT, TEXTLENS_CORS_ORIGINS. A full queue returns 503 with Retry-After.

Exposing the server? Always set TEXTLENS_API_KEYS and put it behind TLS. See the security guide before accepting URL inputs.
Server guide →

MCP for AI agents#

Run TextLens as a Model Context Protocol server so Claude and other MCP clients can OCR, inspect and extract from files inside a workspace folder you choose.

bash
pip install "textlens-ocr[mcp]"
textlens mcp --workspace .

Add it to your client's config, for example .mcp.json:

json
{
  "mcpServers": {
    "textlens": {
      "command": "textlens",
      "args": ["mcp", "--workspace", "."]
    }
  }
}

Tools: ocr_document, inspect_document, extract_tables, extract_fields.

MCP example →

Custom backends#

Add your own engine by subclassing OCRBackend and registering a ModelSpec describing its hardware, tasks and license — the router then treats it like any built-in model. Packages can register through the textlens.models entry point.

python
from textlens import OCRBackend, ModelSpec, register_spec

class MyBackend(OCRBackend):
    def _load(self): ...
    def _recognize(self, image, **kwargs): ...   # return a PageOCR
Custom backend example →

Edge & Raspberry Pi#

The same API runs on a Raspberry Pi 4/5, a Jetson or an industrial PC. The edge stack is PP-OCRv6 on ONNX Runtime with numpy and Pillow — no PyTorch, no OpenCV.

bash
pip install textlens-ocr
textlens setup --profile edge --yes     # download + verify the model, save the profile
textlens ocr sample.jpg
Offline devices: install once with network, or copy ~/.cache/textlens/models from another machine, then set TEXTLENS_OFFLINE=1.

Use a 64-bit OS (Raspberry Pi OS 64-bit or Ubuntu arm64) — ONNX Runtime has no 32-bit ARM wheels.

Raspberry Pi guide →

NVIDIA GPUs#

Install a CUDA build of PyTorch first, then the gpu extra. textlens doctor prints the exact command for your driver.

bash
pip install torch --index-url https://download.pytorch.org/whl/cu124
pip install "textlens-ocr[gpu]"
textlens doctor --deep
textlens models install glm-ocr
python
OCR(profile="balanced")       # PP-OCR for easy pages, GLM-OCR for hard ones
OCR(model="glm-ocr")          # always use GLM-OCR

To run PP-OCR on the GPU too, replace onnxruntime with onnxruntime-gpu.

GPU guide →

Docker & Kubernetes#

Docker is optional. The CPU image contains the server and the default model, with no PyTorch.

bash
git clone https://github.com/Srevarshan05/textlens.git && cd textlens
docker build -f deploy/docker/Dockerfile -t textlens .
docker run -p 8000:8000 -e TEXTLENS_API_KEYS=change-me textlens
FileContents
deploy/docker/DockerfileCPU server image, tested in CI
deploy/docker/Dockerfile.gpuCUDA PyTorch base with the gpu extra (reference)
deploy/docker/Dockerfile.jetsonL4T JetPack base (reference)
deploy/docker/docker-compose.ymlServer with persistent model and data volumes
deploy/kubernetes/textlens.yamlDeployment, Service, health probes and autoscaler
Docker guide →

Upgrading from 0.x#

Your 0.x code keeps working: OCR(model=…).read(), the textlens read command and the /api/v1 endpoints are still there. For new code, call the OCR object directly — it returns the full result instead of a string.

python
# before (0.x)
text = OCR(model="glm-ocr").read("scan.png")

# after (2.0)
result = OCR()("scan.png")       # routed; runs without a GPU
text = result.text

The default model changed from GLM-OCR (GPU) to PP-OCRv6 (CPU). To keep GLM-OCR, pass model="glm-ocr" or use profile="accurate" with it installed.

Migration guide →