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.
No sections match your search.
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.
pip install textlens-ocr
textlens doctor # check hardware, runtimes and modelstextlens-ocr, the import is textlens. pip install textlens installs an unrelated project.Optional extras
| Extra | Adds | Install |
|---|---|---|
| gpu | Document VLMs (GLM-OCR, LightOnOCR, HunyuanOCR, SmolVLM) via PyTorch | pip install "textlens-ocr[gpu]" |
| server | REST API (textlens serve) | pip install "textlens-ocr[server]" |
| documents | DOCX and PPTX input | pip install "textlens-ocr[documents]" |
| anpr | Number-plate detection and recognition | pip install "textlens-ocr[anpr]" |
| mcp | MCP server for AI agents | pip install "textlens-ocr[mcp]" |
| ui | Rich terminal output, system telemetry | pip install "textlens-ocr[ui]" |
| vllm | vLLM for self-hosting large models (Linux) | pip install "textlens-ocr[vllm]" |
| all | Everything above except vllm | pip 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.
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.
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:
textlens ocr invoice.pdf
textlens ocr invoice.pdf -f markdown -o invoice.md
textlens setup # optional: detect hardware, choose a profile, install modelsConfiguration#
Settings come from arguments first, then environment variables, then the config file written by textlens setup.
| Variable | Default | Meaning |
|---|---|---|
| TEXTLENS_HOME | ~/.cache/textlens | Base folder for models, cache and config |
| TEXTLENS_MODELS_DIR | $TEXTLENS_HOME/models | Where model weights are stored |
| TEXTLENS_PROFILE | auto | Default routing profile |
| TEXTLENS_DEVICE | auto | cpu, cuda, cuda:N, tensorrt, coreml, dml |
| TEXTLENS_OFFLINE | 0 | 1 turns every download into an error with instructions |
| TEXTLENS_RESULT_CACHE | memory | Result cache: off, memory or disk |
| TEXTLENS_NO_PERSIST | 0 | 1 never writes results to disk |
| TEXTLENS_MAX_PAGES / _MAX_FILE_MB | — | Input limits |
| TEXTLENS_ALLOW_URLS | 1 | Accept http(s) inputs in Python and the CLI (the server has its own switch) |
| TEXTLENS_LOG_LEVEL / _LOG_FORMAT | WARNING / text | json 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.
| Problem | Fix |
|---|---|
No matching distribution for textlens | The package is textlens-ocr. |
| onnxruntime fails on a Raspberry Pi | Use a 64-bit OS; there are no 32-bit ARM wheels. |
| A scanned PDF returns empty text | Run textlens inspect file.pdf; force OCR with OCR(ocr="force") or --ocr force. |
| GPU present but runs on CPU | Your PyTorch is a CPU build. textlens doctor prints the CUDA install command. |
| PP-OCR should use the GPU | pip uninstall -y onnxruntime && pip install onnxruntime-gpu — never both. |
| CUDA out of memory | Use a smaller profile or OCR(memory_budget_gb=4). |
| Low-confidence pages | result.pages_needing_review() lists them; add a fallback model. |
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.
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/")
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.
| Reason | Meaning |
|---|---|
| scanned | The page is an image with no usable text |
| no_text | No text layer at all |
| vector_text | Text drawn as paths (outlined fonts) |
| invisible_text_layer | A scanner's hidden OCR layer over an image |
| suspected_garbled_text | Broken font encoding — the text would be gibberish |
| sparse_text_over_image | A few words over a mostly-image page |
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
textlens inspect report.pdf
textlens inspect report.pdf --overlay out/ # draw blocks in reading order
textlens ocr report.pdf --ocr forceProfiles & 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.
| Profile | Use it when | Behaviour |
|---|---|---|
| auto | Default | Picks one of the profiles below from your hardware and installed models |
| edge | Raspberry Pi, Jetson, low RAM | ONNX PP-OCR only, 150 dpi, small detector input |
| fast | CPU laptops, high volume | PP-OCR on CPU or GPU, 200 dpi |
| balanced | A GPU with a VLM installed | PP-OCR for simple pages, a VLM for tables, formulas and hard pages |
| accurate | Quality first | Best installed model first, lighter ones as fallback |
| document | PDF → structured Markdown | Generative document models first |
| server | An inference server is configured | Prefers the remote endpoint, high concurrency |
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())
Models#
A small, curated catalog covers different hardware and tasks. Each model has a card with its capabilities, hardware needs, license and known limitations.
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
| Model | Runs on | Tasks | License |
|---|---|---|---|
| ppocrv6-small default | Any CPU, Pi, Jetson | Text lines, boxes, confidence | Apache-2.0 |
| glm-ocr | GPU ≥ 4 GB | Text, tables, formulas, Markdown | MIT |
| lighton-ocr | GPU ≥ 6 GB | Text, tables, formulas, Markdown | Apache-2.0 |
| hunyuan-ocr | GPU ≥ 6 GB | Layouts, charts, extraction | Tencent community (restricted) |
| smolvlm | CPU or small GPU | Rough text | Apache-2.0 |
| paddleocr-vl served | GPU server (vLLM…) | Text, tables, formulas, charts; 100+ languages | Apache-2.0 |
| deepseek-ocr-2 served | GPU server ≥ 10 GB | Dense long documents | Apache-2.0 |
| deepseek-ocr served | GPU server ≥ 10 GB | Documents | MIT |
Served models run in an inference server such as vLLM or SGLang. Point TextLens at it:
OCR(model="paddleocr-vl", endpoint="http://gpu-box:8000/v1")
textlens models info <name> prints the license and notes.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.
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, bboxtextlens extract invoice.pdf -s "invoice_number,date,total"RAG chunks#
Turn any document into section-aware chunks with page numbers and bounding boxes, ready for a vector store and for showing citations.
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 ]
Batch processing#
Process folders, globs or lists. One model copy is shared across files; errors are returned per file instead of stopping the run.
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)
textlens batch ./documents -o results/ # opens a live dashboard
textlens batch ./documents -o results/ --no-dashboardThe 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.
# 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.
CLI#
Run textlens <command> --help for all options.
| Command | What 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 FILE | Structure-first conversion to Markdown, JSON or HTML |
| textlens inspect FILE | Which pages need OCR and why, without OCR. --overlay DIR |
| textlens extract FILE -s … | Extract fields to JSON |
| textlens batch DIR -o OUT | Process a folder, with an optional live dashboard |
| textlens models … | list, search, info, install, remove, verify, path |
| textlens doctor | Diagnose hardware, runtimes and model fit. --deep, --json |
| textlens setup | Guided setup: detect hardware, choose a profile, install models |
| textlens serve | Run the REST API server |
| textlens mcp | Run the MCP server for AI agents |
| textlens benchmark | Compare models on your own labelled data (CER, WER, latency, memory) |
| textlens profile FILE | Per-stage timings, model load time, peak RAM/VRAM |
| textlens anpr FILE | Number-plate recognition |
| textlens cache | Result cache stats or clear |
Python API#
Everything public is importable from textlens. Importing it loads no ML runtime — models load on first use.
| Call | Returns |
|---|---|
| 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.
REST API#
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": "…"}
| Endpoint | Purpose |
|---|---|
| POST /ocr | Image, PDF, DOCX or PPTX → result (format=json|text|markdown|html) |
| POST /document | Structure-first processing (Markdown by default) |
| POST /extract | schema → JSON fields |
| POST /inspect | PDF classification, no OCR |
| POST /batch | Many files → async job |
| GET /jobs/{id}, /jobs/{id}/result | Job status and result; DELETE cancels |
| GET /models, /system | Catalog and hardware status |
| GET /health, /ready, /metrics | Liveness, readiness, Prometheus metrics |
| POST /v1/chat/completions | OpenAI-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.
TEXTLENS_API_KEYS and put it behind TLS. See the security guide before accepting URL inputs.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.
pip install "textlens-ocr[mcp]"
textlens mcp --workspace .Add it to your client's config, for example .mcp.json:
{
"mcpServers": {
"textlens": {
"command": "textlens",
"args": ["mcp", "--workspace", "."]
}
}
}Tools: ocr_document, inspect_document, extract_tables, extract_fields.
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.
from textlens import OCRBackend, ModelSpec, register_spec class MyBackend(OCRBackend): def _load(self): ... def _recognize(self, image, **kwargs): ... # return a PageOCR
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.
pip install textlens-ocr
textlens setup --profile edge --yes # download + verify the model, save the profile
textlens ocr sample.jpg~/.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.
pip install torch --index-url https://download.pytorch.org/whl/cu124
pip install "textlens-ocr[gpu]"
textlens doctor --deep
textlens models install glm-ocrOCR(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.
Docker & Kubernetes#
Docker is optional. The CPU image contains the server and the default model, with no PyTorch.
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
| File | Contents |
|---|---|
| deploy/docker/Dockerfile | CPU server image, tested in CI |
| deploy/docker/Dockerfile.gpu | CUDA PyTorch base with the gpu extra (reference) |
| deploy/docker/Dockerfile.jetson | L4T JetPack base (reference) |
| deploy/docker/docker-compose.yml | Server with persistent model and data volumes |
| deploy/kubernetes/textlens.yaml | Deployment, Service, health probes and autoscaler |
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.
# 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.