hsa-app/receiptscan/README.md
Jean-Michel Tremblay 5acef6869a
All checks were successful
Build and Test / build-and-test (push) Successful in 39s
Add receiptscan pilot (Go + Claude cloud API)
Pilot tool that sends a receipt photo to Claude, classifies the budget
category, and for groceries locates + categorizes each line item, rendering
an annotated PNG with per-category subtotals. Uses the Claude cloud API
(claude-opus-4-8) for pixel-accurate bounding boxes; reads CLAUDE_API_KEY
from ../.env.

Complementary to the local-only OCR/ tools (ollama, no cloud).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:48:28 -04:00

3.6 KiB
Raw Blame History

receiptscan

Sends a receipt photo to Claude, classifies the receipt into a budget category, and (for groceries) locates + categorizes every line item, then renders an annotated PNG: each line item is lightly shaded by food category with a category tag in the margin, plus a per-category subtotal sidebar.

Run

/home/jm/.local/go/bin/go -C /home/jm/programming/HSAmanager/receiptscan run .
  • Image path is hard-coded to /home/jm/aldi.jpg (or pass one as an argument).
  • Reads CLAUDE_API_KEY from ../.env (or ANTHROPIC_API_KEY in the env).
  • Output: <image>_annotated.png next to the input.
  • Go is installed locally at /home/jm/.local/go (no root). GOTOOLCHAIN=local avoids auto-downloading a toolchain.

Useful env flags

  • RECEIPT_CACHE=read — re-render from the saved <image>.cache.json without calling the API (fast iteration on rendering).
  • RECEIPT_DEBUG=1 — print the raw API response, the row-fit, and per-item row assignments to stderr.
  • RECEIPT_ANALYZE=1 — dump detected text rows vs. model boxes and exit.

Model query

  • Model: claude-opus-4-8 (returns true 1:1 image-pixel coordinates; Haiku does not localize accurately and produced vertically-stretched boxes).
  • Adaptive thinking + output_config.effort: high. Without thinking the model often returned a placeholder/empty list.
  • Structured output via output_config.format (json_schema, see schema in main.goanalyze): receipt_type, currency, total_amount, and line_items[] of {name, price, category, box{x,y,width,height}}.
  • The model intermittently returns an empty line_items for a grocery receipt, so the call is retried up to 4× until items come back.

Prompt

You are reading a photographed grocery store receipt. The image is exactly
<W> pixels wide and <H> pixels tall. All coordinates you return must be in
pixels in that space, with (0,0) at the top-left.

1. Set receipt_type to "groceries" and read the grand total (total_amount)
   with its currency.

2. This receipt has many purchased line items (dozens). Return EVERY one of
   them - do not stop early and do not return an empty list. For each line item:
   - name and price exactly as printed,
   - a tight pixel rectangle (box) enclosing that whole printed line, from the
     item name on the left through its price on the right,
   - a food category, exactly one of: dairy, meat, bakery_bread, berries,
     non_berry_fruits, vegetables, desserts, processed_food, pet_stuff,
     other_groceries.
   Use other_groceries only when nothing more specific fits. Do NOT include
   subtotal, tax, payment, or total rows as line items.

Return only the structured object.

Budget categories for receipt_type: house_maintenance, activities, restaurant, groceries, hobbies, alcohol, medical, education, other.

How box alignment works

The model's returned y-coordinates are vertically stretched (~1.35× the true line pitch) and only roughly placed, so boxes are not drawn at the model coordinates. Instead the image itself is used to place them:

  1. Detect horizontal text-row bands (dark-pixel projection in the item column).
  2. Keep only item rows: bands with ink in the price column (right) AND the item-code column (far left), at or below the model's first item. This excludes centered headers, weight/quantity sub-lines, paper-edge shadows, and the payment/total block.
  3. The first N price-bearing rows (N = item count from the model) are the N items, in order — map item i → row i.

This keeps alignment correct regardless of the model's coordinate drift.