From da538a99ee4e5765b1e4904e932a0de9cc4d4daa Mon Sep 17 00:00:00 2001 From: McElwain Date: Sat, 30 May 2026 21:03:41 -0500 Subject: [PATCH] Derive candidate fields from vision crop OCR --- app/logic/vision_analysis.py | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/app/logic/vision_analysis.py b/app/logic/vision_analysis.py index 327c9d8..50b9118 100644 --- a/app/logic/vision_analysis.py +++ b/app/logic/vision_analysis.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Any import hashlib import tempfile +import re try: import fitz # PyMuPDF @@ -619,6 +620,82 @@ def classify_and_crop_unmatched_regions( "classified_regions": classified, } + +def build_vision_candidate_fields(classification: dict[str, Any]) -> list[dict[str, Any]]: + """ + Convert crop OCR/classification results into lightweight structured field candidates. + """ + fields: list[dict[str, Any]] = [] + regions = classification.get("classified_regions") or [] + + money_re = re.compile(r"(?= 70 else 0.45, + }) + + if time_re.search(text): + fields.append({ + **base, + "candidate_type": "transaction_time", + "value": time_re.search(text).group(0), + "raw_text": text, + "confidence": 0.80 if (conf or 0) >= 70 else 0.50, + }) + + if item_count_re.search(text): + fields.append({ + **base, + "candidate_type": "item_count", + "value": item_count_re.search(text).group(0).upper(), + "raw_text": text, + "confidence": 0.65 if (conf or 0) >= 50 else 0.40, + }) + + money_matches = money_re.findall(text) + if money_matches: + fields.append({ + **base, + "candidate_type": "money_amounts", + "value": money_matches, + "raw_text": text, + "confidence": 0.65 if (conf or 0) >= 50 else 0.35, + }) + + # Capture low-value symbol/noise so later filtering can learn from it. + if len(text) <= 3 and not money_matches and not time_re.search(text): + fields.append({ + **base, + "candidate_type": "symbol_or_noise", + "value": text, + "confidence": 0.20, + }) + + return fields + def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_result: dict[str, Any]) -> dict[str, Any]: """ Convert vision analysis into normal layout_json. @@ -638,6 +715,7 @@ def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_re layout, region_score, ) + candidate_fields = build_vision_candidate_fields(region_classification) layout["vision_assisted"] = True layout["vision_assisted_status"] = normalized_vision.get("status", "unknown") @@ -646,6 +724,7 @@ def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_re layout["vision_coordinate_normalization"] = normalized_vision.get("coordinate_normalization") layout["vision_region_score"] = region_score layout["vision_region_classification"] = region_classification + layout["vision_candidate_fields"] = candidate_fields layout["layout_sync_source"] = "vision_assisted" layout["layout_needs_review"] = True return layout