| static | ||
| .dockerignore | ||
| .gitignore | ||
| aliases.py | ||
| Dockerfile | ||
| dryrun.py | ||
| dryrun_match.py | ||
| entrypoint.sh | ||
| extractors.py | ||
| grocy.py | ||
| htmltext.py | ||
| imap_source.py | ||
| LICENSE | ||
| llm.py | ||
| ocr.py | ||
| pipeline.py | ||
| preprocess.py | ||
| README.md | ||
| requirements.txt | ||
| server.py | ||
| store.py | ||
| tasks.py | ||
Provisions
Turn grocery receipts — emailed or photographed — into Grocy stock, on your own hardware, with a human-in-the-loop review step and local AI doing the messy name-matching. No cloud services, no retailer APIs (none of them offer one), nothing leaves your network.
📖 Setup, configuration, and a full how-to-use guide live in the Wiki. This page is just the what-and-why.
What it does
Grocery stores don't give you an API, but they'll email you a receipt — or you can just snap a photo of the paper one. Provisions reads either, figures out what each line item actually is, and adds it to your Grocy inventory with the price and purchase date — after you approve it.
emailed receipt ─IMAP─▶ per-store extractor ─┐
├─▶ local LLM: clean name + category + store
photo of receipt ─▶ OpenCV ─▶ OCR ─▶ text ───┘ │
you review / edit / approve (installable PWA)
│
▼
Grocy stock + price
Two ways in
📧 Emailed receipts are parsed by per-store extractors, then a local LLM cleans the names and categorizes each line.
📷 Photographed receipts (the paper kind — Aldi, the corner store, anything) go through a grounded two-stage pipeline instead of a single "AI, read this image" guess:
Why two stages, and why it matters
A general vision model asked to read a receipt in one pass tends to hallucinate prices and silently drop lines when the photo is faded or skewed — on a real test, one such model returned only 3 of 10 items. Provisions instead:
- Preprocesses the photo (deskew, upscale, gentle contrast) with OpenCV — conservatively, so it never destroys faint thermal print.
- OCRs it with a dedicated text-recognition model (PP-OCR via ONNX, CPU-friendly), which produces grounded text plus a per-line confidence score.
- Structures that real text with a local LLM into
{name, qty, price}— a text task, not an image one, so it can't invent products that aren't on the page.
Low-confidence lines are flagged in the review UI so you can double-check them, and the heavy OCR runs as a background job so big uploads never time out behind a reverse proxy.
Handles real-world receipt mess
Emailed formats supported out of the box, in increasing order of nastiness:
| Source | Looks like | Notes |
|---|---|---|
| Shaw's e-receipt | Signature Select … / $2.49 / Quantity: 1 |
clean, full names |
| Stop & Shop via DoorDash | 1x Store Brand Brown Eggs (12 ct) $2.89 |
delivery; skips out-of-stock items |
| Stop & Shop in-store (Ecrebo) | MARS SNKR 3.29Z 2.79 B + SAVINGS / PRICE YOU PAY |
register abbreviations; discounts netted |
| Amazon order email | truncated names, split prices | non-grocery orders auto-skipped |
That third one is why this exists: MARS SNKR 3.29Z → Mars Snickers 3.29 oz, with the
loyalty discount applied to get the price you actually paid. The local LLM expands the
abbreviations; your approvals are remembered in an alias table, so each cryptic code is
only ever decoded once. Photographed receipts learn the same way.
Tracks only what you care about
Each item is classified into food/pantry, pet, cleaning, health & beauty, alcohol, or batteries — or skipped (prepared/hot deli food, coupons, deposits, and fees never reach your inventory). The store name is read from the receipt too, and matched to your existing Grocy shops so the dropdown pre-selects.
You stay in control
Nothing is written to Grocy until you approve it. New or ambiguous items show up in a small, installable web review queue — per line: map to an existing product, create a new one (with the full Grocy field set), skip, or fix qty/price. Known items from past approvals map silently, so over time the review shrinks to just the genuinely new stuff. Wrongly-skipped lines stay visible in a collapsible section so nothing vanishes on you.
Built local-first
Everything runs on your own boxes: IMAP straight to your mailbox, OCR on CPU, the LLM on a
local Ollama (a comma-separated OLLAMA_URL lets you chain a fast GPU
host with an always-on CPU fallback). The only outbound calls are to your Grocy and your
Ollama.
Roadmap
- Emailed-receipt parser — IMAP fetch, per-store extractors, LLM classify + categorize
- Grocy client + learned alias table — match/create products, add stock with price + date
- Installable web review queue (PWA) with full Grocy product-create fields
- Photographed-receipt support — OpenCV → OCR → LLM, async, confidence flags
- Store-name detection from the receipt
- Optional notification on a newly-staged receipt
- Seam-aware stitching for very long multi-photo receipts
Built with
Built with the help of Claude (Claude Opus 4.8). The architecture, per-store extractors, OCR pipeline, and classification prompts were developed collaboratively against real receipts.
Acknowledgements
Stands on the shoulders of some excellent open-source work: RapidOCR (Apache-2.0) and the bundled PaddleOCR PP-OCR models for text recognition, OpenCV (Apache-2.0) for image preprocessing, huey (MIT) for background jobs, and FastAPI for the API. Inventory lives in Grocy; the local LLM runs on Ollama.
License
GPL-3.0. Copyleft — share alike.