Grocery receipts (emailed or photographed) → Grocy stock, with local OCR + LLM and a human-review PWA. Self-hosted.
Find a file
2026-06-27 16:51:39 -04:00
static feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
.dockerignore Step 3a: FastAPI backend (review API, full Grocy product fields w/ help tooltips, +new catalog endpoints, approve->stock+alias, mailbox poller) + Dockerfile 2026-06-24 18:44:23 -04:00
.gitignore Step 1: receipt parser core (IMAP fetch + 3 store extractors + Ollama classify, dry run) 2026-06-24 18:13:48 -04:00
aliases.py Step 2: Grocy client + alias table + two-stage match/classify; add Amazon (4th format) 2026-06-24 18:33:39 -04:00
Dockerfile feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
dryrun.py docs: rebrand to Provisions; sanitize service-URL defaults to localhost 2026-06-27 16:38:44 -04:00
dryrun_match.py docs: rebrand to Provisions; sanitize service-URL defaults to localhost 2026-06-27 16:38:44 -04:00
entrypoint.sh feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
extractors.py Step 2: Grocy client + alias table + two-stage match/classify; add Amazon (4th format) 2026-06-24 18:33:39 -04:00
grocy.py docs: rebrand to Provisions; sanitize service-URL defaults to localhost 2026-06-27 16:38:44 -04:00
htmltext.py Step 1: receipt parser core (IMAP fetch + 3 store extractors + Ollama classify, dry run) 2026-06-24 18:13:48 -04:00
imap_source.py Step 3a: FastAPI backend (review API, full Grocy product fields w/ help tooltips, +new catalog endpoints, approve->stock+alias, mailbox poller) + Dockerfile 2026-06-24 18:44:23 -04:00
LICENSE Step 1: receipt parser core (IMAP fetch + 3 store extractors + Ollama classify, dry run) 2026-06-24 18:13:48 -04:00
llm.py feat: smarter store-name detection on photo receipts 2026-06-27 16:27:09 -04:00
ocr.py feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
pipeline.py docs: rebrand to Provisions; sanitize service-URL defaults to localhost 2026-06-27 16:38:44 -04:00
preprocess.py feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
README.md docs: add acknowledgements for open-source dependencies 2026-06-27 16:51:39 -04:00
requirements.txt feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
server.py feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00
store.py feat: phone-camera receipt scan via gemma3:4b vision (POST /api/scan-photo + Snap button) -> same review pipeline 2026-06-25 12:20:46 -04:00
tasks.py feat: 2-stage photo OCR pipeline (OpenCV→RapidOCR→LLM), async + confidence 2026-06-27 16:18:41 -04:00

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:

  1. Preprocesses the photo (deskew, upscale, gentle contrast) with OpenCV — conservatively, so it never destroys faint thermal print.
  2. OCRs it with a dedicated text-recognition model (PP-OCR via ONNX, CPU-friendly), which produces grounded text plus a per-line confidence score.
  3. 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.29ZMars 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.