Add OCR naming and PNG-to-PDF export (modules 5-7, single-page only)
- 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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
from datetime import datetime
|
||||
|
||||
from paperpod.naming.filename import build_name, dedupe_stem, slugify
|
||||
|
||||
|
||||
def test_slugify_basic():
|
||||
assert slugify("THE HOME DEPOT") == "the_home_depot"
|
||||
assert slugify("Café Déjà-Vu!!") == "caf_d_j_vu"
|
||||
|
||||
|
||||
def test_slugify_empty_falls_back():
|
||||
assert slugify("") == "doc"
|
||||
assert slugify("!!!") == "doc"
|
||||
|
||||
|
||||
def test_slugify_truncates():
|
||||
long_name = "a" * 100
|
||||
assert len(slugify(long_name, max_len=40)) <= 40
|
||||
|
||||
|
||||
def test_build_name_full_metadata():
|
||||
dt = datetime(2026, 1, 1, 12, 0, 0)
|
||||
result = build_name("2023-03-14", "THE HOME DEPOT", dt)
|
||||
assert result.stem == "2023-03-14_the_home_depot"
|
||||
assert result.source == "ocr"
|
||||
|
||||
|
||||
def test_build_name_date_only():
|
||||
dt = datetime(2026, 1, 1, 12, 0, 0)
|
||||
result = build_name("2023-03-14", None, dt)
|
||||
assert result.stem == "2023-03-14_UNSORTED"
|
||||
assert result.source == "ocr"
|
||||
|
||||
|
||||
def test_build_name_vendor_only():
|
||||
dt = datetime(2026, 1, 1, 12, 0, 0)
|
||||
result = build_name(None, "METRO", dt)
|
||||
assert result.stem == "UNSORTED_metro_20260101_120000"
|
||||
assert result.source == "ocr"
|
||||
|
||||
|
||||
def test_build_name_nothing_found():
|
||||
dt = datetime(2026, 1, 1, 12, 0, 0)
|
||||
result = build_name(None, None, dt)
|
||||
assert result.stem == "UNSORTED_20260101_120000"
|
||||
assert result.source == "none"
|
||||
|
||||
|
||||
def test_dedupe_stem_no_collision():
|
||||
used: set[str] = set()
|
||||
assert dedupe_stem("2023-03-14_metro", used) == "2023-03-14_metro"
|
||||
|
||||
|
||||
def test_dedupe_stem_collision_increments():
|
||||
used: set[str] = set()
|
||||
first = dedupe_stem("2023-03-14_metro", used)
|
||||
second = dedupe_stem("2023-03-14_metro", used)
|
||||
third = dedupe_stem("2023-03-14_metro", used)
|
||||
assert (first, second, third) == (
|
||||
"2023-03-14_metro",
|
||||
"2023-03-14_metro_2",
|
||||
"2023-03-14_metro_3",
|
||||
)
|
||||
Reference in New Issue
Block a user