doc-extract
A lightweight Python utility for extracting text as Markdown from images, scans and PDFs. Each page is sent to a vision model of your choice, and the recognized text comes back as Markdown. It does not depend on layout, language or document type.
doc-extract currently supports:
- Images (PNG, JPG, WEBP, GIF, BMP, TIFF)
Prerequisites
- Python 3.9 or newer
- A server with an OpenAI-compatible API and a vision-capable model (for example Ollama, vLLM, LM Studio, a university server or a cloud provider). A text-only model is not enough.
- An API key is optional. Many self-hosted servers do not need one.
If you run your own model, your documents stay on your own network. Nothing leaves your machine or data center.
Installation
pip install doc-extract-llm
Alternatively, you can install it from the source:
git clone https://github.com/hamidmergan/doc-extract.git
cd doc-extract
pip install .
Usage
Command-Line
Print the result in the terminal:
doc-extract scan.pdf --stdout --base-url http://localhost:11434/v1 --model your-vision-model
With several files, each result gets a <!-- file: name --> header.
Write the result to a file instead:
doc-extract scan.pdf --base-url http://localhost:11434/v1 --model your-vision-model
This writes scan.md next to scan.pdf. Several files work the same way:
doc-extract a.pdf b.jpg --base-url http://localhost:11434/v1 --model your-vision-model
Use -o to choose the output. For one file it is a file, for several files it is a folder:
doc-extract scan.pdf -o notes/scan.md --base-url http://localhost:11434/v1 --model your-vision-model
doc-extract a.pdf b.jpg -o output/ --base-url http://localhost:11434/v1 --model your-vision-model
If two inputs share the same name without extension (for example a.pdf and a.jpg), the full name is used instead (a.pdf.md, a.jpg.md). If the target already exists, a number is added (scan (1).md, scan (2).md). A file named explicitly with -o is overwritten. Errors appear on stderr. The exit code is 1 if at least one file failed.
Python API
from doc_extract import extract
text = extract(
"scan.pdf",
base_url="http://localhost:11434/v1",
model="your-vision-model",
)
print(text)
Several files, each handled on its own:
from doc_extract import extract_many
results = extract_many(["a.pdf", "b.jpg"], base_url=..., model=...)
for name, result in results.items():
if result.ok:
print(name, result.text)
else:
print(name, "Error:", result.error)
extract_many keeps the input order. If one file fails, the others continue and the error is stored in that file's result.
For PDFs, a marker such as <!-- page 2 --> is placed before each page. Use page_markers=False to turn it off.
Settings
Order: argument, then environment variable, then default.
| Argument / flag | Environment variable | Default |
|---|---|---|
base_url / --base-url |
DOC_EXTRACT_BASE_URL |
none, required |
model / --model |
DOC_EXTRACT_MODEL |
none, required |
api_key / --api-key |
DOC_EXTRACT_API_KEY (then OPENAI_API_KEY) |
optional |
max_file_mb / --max-file-mb |
DOC_EXTRACT_MAX_FILE_MB |
20 |
max_pages / --max-pages |
DOC_EXTRACT_MAX_PAGES |
50 |
max_files / --max-files |
DOC_EXTRACT_MAX_FILES |
20 |
max_image_side / --max-image-side |
DOC_EXTRACT_MAX_IMAGE_SIDE |
2000 pixels |
concurrency / --concurrency |
DOC_EXTRACT_CONCURRENCY |
4 |
timeout / --timeout |
DOC_EXTRACT_TIMEOUT |
120 seconds |
prompt / --prompt |
DOC_EXTRACT_PROMPT |
default prompt |
If a limit is exceeded, the file is rejected with a message naming the limit and the setting. Nothing is silently truncated. Only oversized images are downscaled before sending (longest side max_image_side).
Limitations
Quality depends on the chosen model. A small model may misread handwriting, poor photos and very dense tables. Check important results.
Running Tests
pip install -e ".[test]"
pytest
The tests run against a local fake server and need no real model.
License
MIT
Metadata
Release files for doc-extract-llm 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| doc_extract_llm-0.1.0.tar.gz | 14.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| doc_extract_llm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.3 kB
Release files / doc_extract_llm-0.1.0.tar.gz
| Download URL | doc_extract_llm-0.1.0.tar.gz |
|---|---|
| Size | 14.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1884bf4975ca50fb22795f0caffb5f5e436d571adffc8f2a1c255e7e1053022d
|
|
BLAKE2b-256 checksum How to use checksums |
b41467264053c6c3fe4d43e198b2e83bf6df20187c4875ebebd2516c50cbdbbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / doc_extract_llm-0.1.0-py3-none-any.whl
| Download URL | doc_extract_llm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7d17696a7112845c8c466d75c1b75ae05df1080dc3516236aea8de8436718be7
|
|
BLAKE2b-256 checksum How to use checksums |
c4bbba371b333f5b8ca285d8d02905546e11d16ff110d05d6a75e0b5865f2442
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log