All checks were successful
Build and Test / build-and-test (push) Successful in 39s
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>
3.6 KiB
3.6 KiB
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_KEYfrom../.env(orANTHROPIC_API_KEYin the env). - Output:
<image>_annotated.pngnext to the input. - Go is installed locally at
/home/jm/.local/go(no root).GOTOOLCHAIN=localavoids auto-downloading a toolchain.
Useful env flags
RECEIPT_CACHE=read— re-render from the saved<image>.cache.jsonwithout 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 inmain.go→analyze):receipt_type,currency,total_amount, andline_items[]of{name, price, category, box{x,y,width,height}}. - The model intermittently returns an empty
line_itemsfor 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:
- Detect horizontal text-row bands (dark-pixel projection in the item column).
- 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.
- 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.