Fix clipped receipt margins, add time-of-day naming, speed up rotation detection

- vision/document.py: pad_quad() expands the detected quad outward
  before warping so a straight-line contour fit doesn't clip the first
  character of every line on a slightly curled/wrinkled receipt
- ocr/extract.py + llm/vision.py: extract transaction time (HH:MM) so
  same-day repeat visits to a vendor get distinct filenames
  (2023-03-20_1525_walmart.pdf vs. a later same-day trip)
- vision/orient.py: downscale before Tesseract OSD/confidence-sweep
  rotation detection — this was the dominant cost in the detection
  phase (7x speedup: 472s -> 67s on a 36s test video)
This commit is contained in:
2026-07-08 18:55:37 -04:00
parent a43ab20df6
commit 5be9d5935d
11 changed files with 160 additions and 12 deletions
+18 -5
View File
@@ -27,6 +27,9 @@ class NamingResult:
source: str # "ocr" | "none" (will include "speech" once module 4 exists)
_TIME_RE = re.compile(r"^([01]\d|2[0-3]):([0-5]\d)$")
def build_name(
date: str | None,
vendor: str | None,
@@ -35,11 +38,21 @@ def build_name(
form_code: str | None = None,
tax_year: str | None = None,
org_name: str | None = None,
time: str | None = None,
) -> NamingResult:
"""date must already be normalized to YYYY-MM-DD, or None."""
"""date must already be normalized to YYYY-MM-DD, time to HH:MM, or None.
Time is appended to the date (e.g. 2023-03-20_1525) whenever it was
extracted, so two same-day visits to the same vendor (Walmart in the
morning and again in the evening) don't collide or silently overwrite.
"""
vendor_slug = slugify(vendor, max_vendor_len) if vendor else None
ts = fallback_dt.strftime("%Y%m%d_%H%M%S")
date_part = date
if date and time and _TIME_RE.match(time):
date_part = f"{date}_{time.replace(':', '')}"
# Tax slips: prefer 2023_T4_York_University over UNSORTED_t4_york_university_<timestamp>.
if form_code:
org_slug = slugify(org_name, max_vendor_len) if org_name else None
@@ -58,10 +71,10 @@ def build_name(
return NamingResult(stem=f"{code}_{org_slug}", source="ocr")
return NamingResult(stem=code.lower(), source="ocr")
if date and vendor_slug:
return NamingResult(stem=f"{date}_{vendor_slug}", source="ocr")
if date:
return NamingResult(stem=f"{date}_UNSORTED", source="ocr")
if date_part and vendor_slug:
return NamingResult(stem=f"{date_part}_{vendor_slug}", source="ocr")
if date_part:
return NamingResult(stem=f"{date_part}_UNSORTED", source="ocr")
if vendor_slug:
return NamingResult(stem=f"UNSORTED_{vendor_slug}_{ts}", source="ocr")
return NamingResult(stem=f"UNSORTED_{ts}", source="none")