Extracts structured data (vendor, totals, line items) from invoices, bills, and receipts — PDFs or scanned images — using local LLMs. No cloud API keys: every model runs through Ollama on your own machine/GPU.
Two front ends share the same extraction core:
app.py— a Streamlit desktop-style UI. Single process, synchronous, good for quick local use.manage.py/config//processor/— a Django + Channels web app with a background job queue and live progress over WebSockets. Good for longer-running or multi-page jobs where you want to watch progress and cancel mid-run.
file (PDF/image)
│
▼
OpenCV preprocessing (extractor.py)
— deskew/denoise scanned images, CLAHE contrast for clean scans
▼
Docling text extraction (extractor.py)
— digital-PDF fast path (no OCR) first
— falls back to OCR if the page is a scan or came back empty
— falls back again to rendering the page as a bitmap if Docling
swallowed a table as an embedded image
▼
LLM extraction — one of two pipelines below (Ollama)
▼
JSON repair + amount sanitisation
— 3-tier JSON repair: direct parse → json_repair → bracket-closing
— comma/period locale correction and ×100 OCR-error correction
▼
structured invoice JSON (vendor, totals, line_items[])
Single-pass extraction, tuned for a ~40s/page budget:
- One prompt per page asks for the full schema (header, totals, line items).
- Multi-page documents split the work: a fast 3B model (
FAST_MODEL) pulls the header and totals once, while each page's line items are extracted concurrently. - If a page returns no line items (e.g. a dense table the LLM under-extracted),
a regex-based fallback (
extract_line_items_fallback) detects table rows by pattern and re-asks the LLM in batches of 60 rows. - Model chaining:
PRIMARY_MODEL(qwen2.5:7b) is tried first; any exception (timeout, bad output, OOM) triggers one automatic fallback toFALLBACK_MODEL(llama3.1:8b) for the rest of the job.
Used when the user selects "High Accuracy" mode. Trades speed for reliability:
- Runs the same extraction 3 times with a larger 40k-char context window and slightly increasing temperature (0.05 → 0.15) for diversity between passes.
- Votes across the three candidates — numeric fields take the median, string fields take the majority value, nested objects (vendor/bill-to) take the most complete candidate, and line items take the longest list.
- Validates the math: sums line-item totals against tax/shipping/discount and the stated grand total. If they don't reconcile, appends a labelled "reconciliation" line item so the table always ties out, instead of silently showing a wrong total.
| Model | Role |
|---|---|
qwen2.5:7b |
Primary — best structured-output accuracy |
qwen2.5:3b |
Fast — header/totals only |
llama3.1:8b |
Automatic fallback if the primary fails |
All models run locally via Ollama; nothing is sent to an external API.
- Install Ollama and pull the models:
ollama pull qwen2.5:7b ollama pull qwen2.5:3b ollama pull llama3.1:8b - Install Python dependencies:
pip install -r requirements.txt - Sanity-check your environment:
python verify_env.py
streamlit run app.py
python manage.py migrate
python manage.py runserver
(Uses Daphne/Channels for ASGI + WebSockets, so runserver serves both HTTP
and the ws/jobs/ live-progress socket.)
- GPU is optional.
devicecan be forced tocpu,cuda, or left onautodetection; tunePAGE_TOKENS_PER_SECONDinllm.pyto your own GPU's measured throughput. config/settings.pyships withDEBUG = Trueand a placeholderSECRET_KEYfor local development — set a real secret key andDEBUG = Falsebefore deploying anywhere public.