Metadata-Version: 2.5
Name: receipt-reader
Version: 0.1.0
Summary: Analyze a shopping receipt photo locally and break its lines down into categories and subcategories.
Project-URL: Homepage, https://github.com/dannyzimm/receipt-reader
Project-URL: Repository, https://github.com/dannyzimm/receipt-reader
Project-URL: Issues, https://github.com/dannyzimm/receipt-reader/issues
Author: Daniel Pérez-Rodríguez
License-Expression: MIT
License-File: LICENSE
Keywords: classification,nimble,ocr,receipt,ticket
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Accounting
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic-settings>=2.2
Requires-Dist: pydantic>=2.6
Requires-Dist: python-dotenv>=0.21
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: flask>=3.0; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: local
Requires-Dist: numpy>=1.26; extra == 'local'
Requires-Dist: opencv-python-headless>=4.9; extra == 'local'
Requires-Dist: rapidocr>=3.9; extra == 'local'
Provides-Extra: nimble
Requires-Dist: httpx>=0.27; extra == 'nimble'
Provides-Extra: preprocess
Requires-Dist: numpy>=1.26; extra == 'preprocess'
Requires-Dist: opencv-python-headless>=4.9; extra == 'preprocess'
Requires-Dist: pillow>=10.0; extra == 'preprocess'
Provides-Extra: surya
Requires-Dist: pillow>=10.0; extra == 'surya'
Requires-Dist: surya-ocr>=0.6; extra == 'surya'
Provides-Extra: tesseract
Requires-Dist: pillow>=10.0; extra == 'tesseract'
Requires-Dist: pytesseract>=0.3; extra == 'tesseract'
Provides-Extra: vlm
Requires-Dist: httpx>=0.27; extra == 'vlm'
Provides-Extra: web
Requires-Dist: flask>=3.0; extra == 'web'
Description-Content-Type: text/markdown

# receipt-reader

Analyze a photo of a shopping receipt **fully locally** and break its lines down
into the categories and subcategories defined in a YAML taxonomy. The output is
structured JSON, designed to be easily embedded into other applications.

## Architecture

The core is decoupled from providers through *ports & adapters*:

```
image → preprocess(OpenCV) → OcrEngine(RapidOCR | Tesseract)
      → reconstruct (rows) → receipt_parser → RawExtraction
      → ItemClassifier(Nimble) → ReceiptResult → JSON
```

- **OCR**: [RapidOCR](https://github.com/RapidAI/RapidOCR) by default (PP-OCR
  models in ONNX, CPU, no CUDA); Tesseract as an alternative. Optionally
  **Surya 2** (layout + reading order + document OCR) for maximum accuracy.
- **Parser**: heuristics for Spanish EUR receipts (quantities, weight, discounts,
  VAT, payment noise), with an OCR-tolerant amount tokenizer and arithmetic
  reconciliation against the total.
- **Classification**: [Nimble 9B](https://ollama.com/library/nimble) served by
  Ollama (`/v1/systemone`), queried over HTTP and suitable for CPU.

Switching OCR engine or classifier is just a matter of configuration; the
pipeline does not change. For maximum accuracy there is a `vlm` extractor
(self-hosted vision VLM via Ollama) and an `ensemble` extractor that combines
engines by consensus.

## Installation

```bash
# Everything needed for local extraction + preprocessing
pip install "receipt-reader[local,preprocess]"

# Tesseract alternative (also requires the tesseract-ocr and spa binaries)
sudo apt install tesseract-ocr tesseract-ocr-spa
pip install "receipt-reader[tesseract]"
```

For development, clone the repository and install it in editable mode:

```bash
pip install -e ".[local,preprocess,nimble,dev]"
```

Extras: `local` (RapidOCR), `tesseract`, `surya`, `preprocess` (OpenCV),
`nimble` (httpx), `vlm`, `web`, `dev`.

RapidOCR downloads its ONNX models the first time it runs.

## Configuration

Copy `.env.example` to `.env`. Everything is optional; the defaults work. **Do
not put comments on the same line as a value** (systemd and some parsers do not
strip them):

```
OCR_ENGINE=rapidocr
OCR_LANG=es
OCR_MODEL_TYPE=small
OCR_VERSION=PP-OCRv6
OCR_USE_GPU=false
TAXONOMY_PATH=
```

`OCR_ENGINE`: `rapidocr` | `tesseract` · `OCR_MODEL_TYPE`: `tiny` | `small` |
`medium` · `OCR_VERSION`: `PP-OCRv4` | `PP-OCRv5` | `PP-OCRv6`.

RapidOCR uses **PP-OCRv6** by default with `lang_type` as an ISO code (`es`). The
first run downloads the corresponding recognition model. If your receipts are
hard, try `OCR_MODEL_TYPE=medium` (more accurate, slower).

Nimble is consumed over HTTP. If it lives on another machine, open an SSH tunnel:

```bash
ssh -L 11434:127.0.0.1:11434 user@nimble-host
```

and set `NIMBLE_BASE_URL=http://localhost:11434`.

## Usage

```bash
# with TAXONOMY_PATH set, -t is not needed
receipt-reader analizar ticket.jpg --pretty
# or passing the taxonomy explicitly
receipt-reader analizar ticket.jpg --taxonomy categories.yaml --pretty
# engine ensemble (consensus): combines the extractors in ENSEMBLE_EXTRACTORS
receipt-reader analizar ticket.jpg -e ensemble --pretty
# classifier with a persistent lexicon (learns from the webapp corrections)
receipt-reader analizar ticket.jpg -c lexicon --pretty
```

To fine-tune the parser on real receipts (prints raw OCR, rows and parsing):

```bash
receipt-reader depurar ticket.jpg --pretty
```

To measure extraction accuracy against a labeled dataset (format in
[`evaluation/README.md`](evaluation/README.md)):

```bash
receipt-reader evaluar evaluation/ --detalle
```

As a library:

```python
from receipt_reader.taxonomy import load_taxonomy
from receipt_reader.registry import make_extractor, make_classifier
from receipt_reader.pipeline import ReceiptPipeline

tax = load_taxonomy("categories.yaml")
pipeline = ReceiptPipeline(make_extractor("local"), make_classifier("nimble"))
result = pipeline.process(open("ticket.jpg", "rb").read(), tax)
print(result.model_dump_json(indent=2))
```

## Output

```json
{
  "comercio": "Mercadona",
  "fecha": "2024-05-02",
  "moneda": "EUR",
  "total": "3.00",
  "lineas": [
    {
      "descripcion": "Leche entera",
      "cantidad": "2",
      "importe": "1.90",
      "categoria_id": "alimentacion",
      "subcategoria_id": "lacteos",
      "confianza": 0.93
    }
  ],
  "totales_por_categoria": {"alimentacion": {"lacteos": "1.90"}},
  "revisar": [],
  "avisos": [],
  "meta": {"proveedor": "local:rapidocr"}
}
```

Amounts and quantities are serialized as **strings** to preserve decimal
accuracy. `revisar` holds the indices of the lines whose category could not be
assigned or fell below the confidence threshold.

## Taxonomy format

```yaml
version: 1
currency: EUR
categories:
  - id: alimentacion
    name: Alimentación
    description: Productos de supermercado para consumo en casa
    subcategories:
      - {id: lacteos, name: Lácteos, description: Leche, yogur, queso...}
```

## Debug web app

`debug_webapp/` contains a small Flask app **isolated from the package** to
upload receipts, inspect the raw OCR, correct the classification by hand and
edit/validate the YAML taxonomies. It lets you pick the **extractor** (`local`,
`surya`, `vlm` or `ensemble`), which enables the multi-engine consensus and the
VLM configured in `.env` from the browser. It is not included in the wheel.

```bash
pip install -e ".[local,preprocess,nimble,web]"
TAXONOMY_PATH=~/receipt-config/household.yaml python -m debug_webapp.app
```

See [`debug_webapp/README.md`](debug_webapp/README.md) for details.

## Tests

```bash
pytest                  # unit and contract tests (no network)
pytest -m integration   # real services (Nimble)
```

## Known limitations

- The parser is tuned for standard Spanish supermarkets; very different formats
  may need adjustments (use `depurar` to iterate).
- Heavily rotated, crumpled or low-contrast photos degrade the OCR beyond what
  the parser can fix; in those cases use the `ensemble` or `vlm` extractor, or
  retake the photo.
- The local extractor does not process PDFs directly: convert them to an image
  first.

## License

MIT.
