Automatic classification and sorting of scanned documents using OCR and local LLM.
DocSort reads scanned documents (PDF, DOCX, XLSX, images, etc.), extracts text via OCR, classifies the document type using an LLM, and automatically sorts files into a consistent folder structure with descriptive filenames.
- Universal document formats — PDF, DOCX, XLSX, PPTX, ODT, JPG, PNG, TIFF and more
- Smart text extraction — PyMuPDF for digital PDFs (<1s), GPU-accelerated OCR via Docling only for scanned documents
- Multi-LLM support — LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini
- Sender detection — Automatically identifies the document sender/issuer
- Consistent naming —
YYYY-MM-DD_Description.ext - Flexible folder structure — Configurable template (
{doc_type}/{year}/{absender}/{filename}etc.) - Configurable filename template —
{doc_date}_{short_info}(default), customizable with variables - Parallel processing — LLM calls run in parallel with configurable worker count (default: 4)
- Text extraction cache — SHA-256 hash-based cache in
.docsort_cache/, skips re-extraction on repeated runs - YAML configuration — All settings persisted in
docsort.yaml - LLM profiles — Quickly switch between providers
- Customizable system prompt — Tailor classification to your needs
- Confidence threshold — Uncertain classifications are flagged
- Retry logic — Automatic retry on LLM errors
- Undo function — Revert operations via undo log
- Copy & Move mode — Originals stay intact (default) or get moved
- Dry-run — Preview without changes
- CLI & Web UI — Command line (Click) or graphical interface (Gradio)
- OCR quality check — Warning on empty or unreadable text
- Watchfolder — Monitor a directory and auto-process new files
- Document preview — PDF/image preview directly in the Web UI
- Fault tolerance — One failed file doesn't stop the rest
This project was developed with AI assistance ("vibe coding") and uses third-party open-source dependencies that have not been independently audited. The software is provided "as is" under the MIT License, without warranty of any kind.
Please note:
- Back up your documents before using DocSort, especially in
--movemode (which relocates originals) - The
--copymode (default) keeps your originals untouched — use this until you trust the results - The
--dry-runflag lets you preview all changes before anything happens - OCR and LLM classification can produce errors — always verify results for important documents
- External dependencies (Docling, Gradio, OpenAI SDK, etc.) are maintained by their respective projects — vulnerabilities in those packages are outside our control
- This tool is a personal/hobby project, not a certified document management system
Short version: Test with copies first, check the results, keep backups. Don't blindly trust AI classification for legally or financially critical documents.
| Component | Version | Note |
|---|---|---|
| Python | 3.11+ | Recommended: 3.12 |
| uv | latest | Package manager (Install) |
| LLM server | — | LM Studio, Ollama (local) or cloud API (OpenAI, Claude, Gemini) |
| NVIDIA GPU | optional | Recommended: RTX 4070 Ti SUPER (12 GB VRAM) |
| CUDA | 12.8 | For GPU acceleration |
| PyMuPDF | latest | Fast text extraction for digital PDFs (installed automatically) |
DocSort supports 5 LLM providers out of the box:
| Provider | Type | Cost | Setup |
|---|---|---|---|
| LM Studio | Local | Free | lmstudio.ai → Load model → Start server |
| Ollama | Local | Free | ollama.com → ollama pull llama3 |
| OpenAI | Cloud | ~$0.003/doc | API key from platform.openai.com |
| Anthropic Claude | Cloud | ~$0.003/doc | API key from console.anthropic.com |
| Google Gemini | Cloud | ~$0.001/doc | API key from aistudio.google.com |
Recommendation: LM Studio or Ollama — free, local, no data leaves your machine.
git clone https://github.com/ichabot/docsort.git
cd docsortuv venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windowsuv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128No GPU? Skip this step — DocSort automatically falls back to CPU.
# Standard installation (OpenAI-compatible LLMs)
uv pip install -e .
# With Anthropic Claude support
uv pip install -e ".[anthropic]"
# With everything (Anthropic + dev tools)
uv pip install -e ".[all-llm,dev]"uv pip install "docling-ocr-onnxtr[gpu]"docsort initCreates a docsort.yaml with all settings. Alternatively, use docsort.example.yaml as a template.
DocSort is configured via a YAML file. Search paths:
./docsort.yaml(current directory)~/.config/docsort/docsort.yaml
# Active LLM profile
active_profile: lm-studio
# Add/customize profiles
profiles:
openai:
api_key: sk-your-key-here
anthropic:
api_key: sk-ant-your-key-here
my-server:
provider: openai
base_url: http://192.168.1.100:8080/v1
model: my-model
api_key: optional
description: My own LLM server
# Output
output_dir: ./sorted
mode: copy
# Folder structure template
folder_template: "{doc_type}/{year}/{absender}/{filename}"
# Filename template
filename_template: "{doc_date}_{short_info}"
# Parallel processing
max_workers: 4
# Extraction cache directory
cache_dir: .docsort_cache
# Quality
confidence_threshold: 0.7
max_retries: 2
# Enable undo log
undo_log: ./docsort_undo.csvBuilt-in profiles:
| Profile | Provider | URL | Default model |
|---|---|---|---|
lm-studio |
openai | localhost:1234/v1 |
(LM Studio default) |
ollama |
openai | localhost:11434/v1 |
llama3 |
openai |
openai | api.openai.com/v1 |
gpt-4o-mini |
anthropic |
anthropic | api.anthropic.com |
claude-sonnet-4-20250514 |
gemini |
openai | generativelanguage.googleapis.com/... |
gemini-2.0-flash |
Show profiles:
docsort profilesThe template determines the folder structure. Available variables:
| Variable | Description | Example |
|---|---|---|
{doc_type} |
Document type | Rechnung |
{absender} |
Sender/issuer | Stadtwerke-Muenchen |
{year} |
Year from document date | 2026 |
{month} |
Month from document date | 03 |
{filename} |
Generated filename | 2026-03-15_Strom-Abrechnung.pdf |
Examples:
# Default: Type → Year → Sender (recommended)
folder_template: "{doc_type}/{year}/{absender}/{filename}"
# → sorted/Rechnung/2026/Stadtwerke-Muenchen/2026-01-15_Strom-Abrechnung.pdf
# Without sender folder
folder_template: "{doc_type}/{year}/{filename}"
# → sorted/Rechnung/2026/2026-01-15_Strom-Abrechnung.pdf
# Year first
folder_template: "{year}/{doc_type}/{absender}/{filename}"
# → sorted/2026/Rechnung/Stadtwerke-Muenchen/2026-01-15_Strom-Abrechnung.pdf
# Flat (no subfolders)
folder_template: "{filename}"
# → sorted/2026-01-15_Strom-Abrechnung.pdfThe filename template controls how sorted files are named. Available variables:
| Variable | Description | Example |
|---|---|---|
{doc_date} |
Document date (YYYY-MM-DD) | 2026-03-15 |
{short_info} |
Short description | Strom-Abrechnung |
{absender} |
Sender/issuer | Stadtwerke-Muenchen |
{doc_type} |
Document type | Rechnung |
{year} |
Year from document date | 2026 |
{month} |
Month from document date | 03 |
Examples:
# Default
filename_template: "{doc_date}_{short_info}"
# → 2026-03-15_Strom-Abrechnung.pdf
# With sender in filename
filename_template: "{doc_date}_{absender}_{short_info}"
# → 2026-03-15_Stadtwerke-Muenchen_Strom-Abrechnung.pdf
# Type prefix
filename_template: "{doc_type}_{doc_date}_{short_info}"
# → Rechnung_2026-03-15_Strom-Abrechnung.pdfThe system prompt controls how the LLM classifies documents. Edit it in the config or Web UI:
system_prompt: |
You are a document classifier for a medical practice.
Analyze the following text and classify the document.
Allowed document types:
{doc_types}
Respond with JSON:
{"doc_type": "...", "absender": "...", "short_info": "...", "doc_date": "YYYY-MM-DD", "confidence": 0.95}Note:
{doc_types}is automatically replaced with the configured document types list.
doc_types:
- Invoice
- Contract
- Letter
- Tax Notice
- Insurance
- Other# Default (uses docsort.yaml)
docsort process ./scans
# Specify output directory
docsort process ./scans -o ./archive
# Dry-run: preview only
docsort process ./scans --dry-run
# With specific LLM profile
docsort process ./scans --profile openai
# Move files instead of copying
docsort process ./scans --move
# All options
docsort process ./scans \
-o ./archive \
--move \
--profile anthropic \
--batch-size 16 \
--verbose| Option | Description | Default |
|---|---|---|
-o, --output |
Output directory | ./sorted |
--copy / --move |
Copy or move files | --copy |
--dry-run |
Preview only | off |
--profile |
LLM profile | from config |
--llm-url |
LLM API URL (overrides profile) | — |
--model |
Model name (overrides profile) | — |
--api-key |
API key (overrides profile) | — |
--no-gpu |
Disable GPU | GPU on |
--batch-size |
OCR batch size | 32 |
-w, --workers |
Number of parallel workers | from config |
--no-cache |
Disable text extraction cache | off |
--max-pages |
Max pages per document (0 = all) | 5 |
--config |
Path to config file | auto |
-v, --verbose |
Verbose output | off |
# Monitor directory (checks every 5 seconds)
docsort watch ./scans -o ./sorted
# With shorter interval
docsort watch ./scans --interval 2
# With specific profile and move mode
docsort watch ./scans -o ./archive --profile openai --moveNew files in the monitored directory are automatically detected, OCR-processed, classified, and sorted. Already processed files are skipped. Stop with Ctrl+C.
# Create config
docsort init
# Show LLM profiles
docsort profiles
# Undo last operations
docsort undo # all
docsort undo -n 5 # last 5
# Clear extraction cache
docsort cache clear# Default on port 7860
docsort web
# Different port
docsort web --port 8080
# Public link
docsort web --shareThe Web UI has four tabs:
- 📁 Processing — Upload files, analyze & execute
- Results table with search bar, per-column filters, and sortable columns (click headers)
- Traffic light confidence status: 🟢 ≥85%, 🟡 50–84%, 🔴 <50%
- Side panel: Click a row → PDF/image preview + edit fields
- Correct document type, sender, description and date
- Apply changes → table updates
- Undo button (↩️ Rückgängig) — revert the last operation directly from the UI
- ⚙️ Settings — LLM profile, output, folder structure, document types, confidence threshold
- 📝 System Prompt — Customize the classification prompt
- ℹ️ Info — Version info, pipeline overview, and help
| Processing | Settings |
|---|---|
![]() |
![]() |
| System Prompt | Info |
|---|---|
![]() |
![]() |
YYYY-MM-DD_Description.ext
Examples:
2026-11-21_Strom-Abrechnung-Januar.pdf2025-06-01_Haftpflicht-Jahresbeitrag.pdf2026-03-10_Einkommensteuer-2025.pdf
Document type and sender are in the folder path and not repeated in the filename.
sorted/
├── Rechnung/
│ └── 2026/
│ ├── Stadtwerke-Muenchen/
│ │ └── 2026-01-15_Strom-Abrechnung.pdf
│ └── Telekom/
│ └── 2026-02-01_Mobilfunk-Februar.pdf
├── Vertrag/
│ └── 2025/
│ └── Allianz-Versicherung/
│ └── 2025-06-01_Haftpflicht.pdf
└── Bescheid/
└── 2026/
└── Finanzamt-Muenchen/
└── 2026-03-10_Einkommensteuer-2025.pdf
- Hyphens instead of spaces
- Umlauts are replaced:
ä→ae,ö→oe,ü→ue,ß→ss - No special characters except hyphens
- Maximum 60 characters
- No date in the description
Rechnung · Quittung · Vertrag · Kuendigung · Brief · Bescheid · Steuerbescheid · Kontoauszug · Lohnabrechnung · Versicherung · Mahnung · Angebot · Lieferschein · Gutschrift · Mietvertrag · Arbeitsvertrag · Zeugnis · Urkunde · Formular · Sonstiges
Custom document types can be defined via config or Web UI.
On name collisions: _2, _3 etc.
docsort/
├── pyproject.toml # Project config & dependencies
├── docsort.yaml # Your config (after docsort init)
├── docsort.example.yaml # Example config template
├── README.md # This file (English)
├── README_DE.md # German documentation
├── LICENSE
├── .gitignore
├── src/
│ └── docsort/
│ ├── __init__.py # Package init + version
│ ├── config.py # YAML config, LLM profiles, load/save
│ ├── extractor.py # Text extraction: PyMuPDF fast-path + Docling OCR fallback + quality check
│ ├── classifier.py # Multi-LLM classification (OpenAI + Anthropic)
│ ├── organizer.py # Rename + sort files + undo log
│ ├── pipeline.py # Orchestrates: extract → classify → organize
│ ├── cli.py # Click CLI (process, web, watch, undo, init, profiles)
│ ├── web.py # Gradio Web UI with side panel + PDF preview
│ └── watcher.py # Watchfolder — automatic processing
└── tests/
├── __init__.py
└── test_pipeline.py # Unit tests (55 tests)
Input File
│
▼
[1. Cache Check] ─── SHA-256 hash lookup in .docsort_cache/
│ ✓ Hit → skip extraction, use cached text
│ ✗ Miss → continue to extraction
▼
[2. Extractor] ─── PyMuPDF (digital PDF) or Docling OCR (scans/images) → Text + Metadata
│ 💾 Store result in cache
│
▼
[3. Classifier] ─── LLM (selectable, parallel workers) → Type, Sender, Info, Date, Confidence
│ ↻ Retry on error
│ ⚠ Warning on low confidence
▼
[4. Organizer] ─── Template → Target path → Copy/Move (sequential)
│ 📝 Write undo log
▼
Sorted file in target folder
# Run tests
pytest
# Verbose output
pytest -v
# With coverage report
pytest --cov=docsort --cov-report=term-missing55 tests covering:
- Description sanitization (umlauts, special characters, length limits)
- JSON extraction (direct, markdown code blocks, embedded)
- Date fallback logic (LLM → file date → today)
- Target path construction with various templates
- Duplicate resolution
- File organization (dry-run, copy, move, undo log)
- File collection (recursive, single file, empty, invalid)
- Config system (profiles, YAML load/save, defaults)
- OCR quality detection (empty, short text, garbage characters)
- Watchfolder (file stability, run-once)
Note: GPU is only needed for scanned documents and images. Digital PDFs with a text layer are processed by PyMuPDF on CPU in under 1 second — no GPU required.
| Parameter | Value |
|---|---|
ocr_batch_size |
16 to 32 |
max_workers |
4 (parallel LLM calls) |
| CUDA Version | 12.8 |
Disable GPU:
docsort process ./scans --no-gpuDocSort automatically detects whether CUDA is available and falls back to CPU if needed.
LLM classification calls run in parallel to speed up batch processing. File organization remains sequential to avoid conflicts.
# In docsort.yaml
max_workers: 4 # Number of parallel LLM calls (default: 4)# Override via CLI
docsort process ./scans --workers 8Tip: Set
max_workersto match your LLM server's concurrency capacity. For cloud APIs (OpenAI, Claude, Gemini), higher values (8–16) work well. For local LLMs, 2–4 is usually optimal.
MIT License — see LICENSE



