- ocr/: Tesseract-based vendor/date/total extraction. PSM 6 (single uniform text block) instead of the default fixes receipts where item-name/price columns were otherwise split into separate blocks and dropped. Vendor line picked by median (not max) word height within the top fraction of the crop, breaking ties by topmost line - max() was fooled by single descender/ascender glyphs (commas, parens) inflating one word's bounding box. - naming/: merge OCR metadata into YYYY-MM-DD_vendor.pdf filenames, degrading gracefully to UNSORTED_<timestamp>.pdf; per-run dedup. - pdf/: img2pdf-based multi-page-capable assembly (images_to_pdf). - pipeline.run_export(): OCR + name + PDF each detected crop from a detect() report, writes pdf/ and export_summary.csv. - New `paperpod export <video>` CLI command. - Verified against synthetic Home Depot/Metro/Petro-Canada receipts: 3/3 correct vendor, date, and total after tuning. Known limitation (documented in README): no page-flip/multi-page grouping yet (that's events/, still unbuilt) - every detected document becomes its own single-page PDF.
56 lines
1.6 KiB
Python
56 lines
1.6 KiB
Python
"""Unit tests for the regex/heuristic parsing logic in paperpod.ocr.extract.
|
|
|
|
These test the parsing functions directly on strings so they stay
|
|
deterministic and don't depend on Tesseract's OCR accuracy. End-to-end OCR
|
|
accuracy against rendered receipts is exercised in test_ocr_integration.py.
|
|
"""
|
|
|
|
from paperpod.ocr.extract import extract_date, extract_total
|
|
|
|
|
|
def test_extract_date_iso():
|
|
assert extract_date("2023-03-14 09:41 #4821") == "2023-03-14"
|
|
|
|
|
|
def test_extract_date_slash_mdy():
|
|
assert extract_date("Purchased on 03/14/2023 at noon") == "2023-03-14"
|
|
|
|
|
|
def test_extract_date_month_name():
|
|
assert extract_date("Invoice date: March 14, 2023") == "2023-03-14"
|
|
assert extract_date("Mar 14 2023") == "2023-03-14"
|
|
|
|
|
|
def test_extract_date_none_when_absent():
|
|
assert extract_date("no date here, just text") is None
|
|
|
|
|
|
def test_extract_date_rejects_implausible_values():
|
|
# Not a real month/day -> not treated as a date.
|
|
assert extract_date("13/45/2023") is None
|
|
|
|
|
|
def test_extract_total_basic():
|
|
text = "SUBTOTAL 72.14\nHST 13% 9.38\nTOTAL 81.52\n"
|
|
assert extract_total(text) == "81.52"
|
|
|
|
|
|
def test_extract_total_ignores_subtotal():
|
|
text = "SUBTOTAL 30.63\n"
|
|
assert extract_total(text) is None
|
|
|
|
|
|
def test_extract_total_word_boundary_with_prefix():
|
|
# "FUEL TOTAL" should still match "total" as its own word.
|
|
text = "FUEL TOTAL 65.10\n"
|
|
assert extract_total(text) == "65.10"
|
|
|
|
|
|
def test_extract_total_amount_on_next_line():
|
|
text = "TOTAL\n81.52\n"
|
|
assert extract_total(text) == "81.52"
|
|
|
|
|
|
def test_extract_total_none_when_absent():
|
|
assert extract_total("no totals here") is None
|