Add crop refinement, finger removal, and local LLM naming

- vision/refine.py: tighten crops to the paper band (removes mat
  margins and hands beside receipts) and inpaint border-connected
  skin regions so fingers disappear from output
- llm/vision.py: identify documents with a local Ollama vision model
  (qwen2.5vl); extracts vendor/date/total/form code and flags quality
  issues (fingers, blur, glare); falls back to Tesseract when down
- pipeline: drop blank pages, dedupe consecutive captures of the same
  document, record refine/LLM fields in report and export summary
This commit is contained in:
2026-07-08 18:23:50 -04:00
parent 2c28a623fb
commit a43ab20df6
22 changed files with 1497 additions and 111 deletions
+42
View File
@@ -31,9 +31,39 @@ class MotionConfig:
@dataclass
class DocumentConfig:
min_area_ratio: float = 0.04
# Candidates covering more than this fraction of the frame are rejected
# as likely background/lighting rather than a document.
max_area_ratio: float = 0.92
# Candidates whose long side is more than this many times their short
# side are rejected (fixes thin sliver/edge-strip false detections).
max_aspect: float = 6.0
canny_low: int = 50
canny_high: int = 150
# Contour search runs on a frame downscaled to this width; edge/threshold
# gap-closing kernels are sized relative to it rather than the source
# resolution, and the winning quad is scaled back up for the actual crop.
# Verified fix: a 4K frame's document outline was fragmenting into
# sub-regions with a fixed-pixel kernel tuned for ~1000px-wide frames.
detect_width: int = 1000
# Bake illumination-normalization + contrast enhancement into the saved
# crop/PDF, not just the internal OCR pass (which always applies it).
enhance: bool = False
# Auto-detect and correct 0/90/180/270 rotation before OCR/PDF export.
# Fixes the "placed the document upside down" case, which otherwise
# produces a fine-looking crop that OCR reads as garbage.
auto_rotate: bool = True
# Tighten each crop to the actual paper region (removes mat margins and
# hands hanging off the side of receipts) and inpaint fingers pressing
# on the page. Content *under* a finger can't be recovered — it's filled
# with plausible paper texture — but margins clean up completely.
refine: bool = True
remove_fingers: bool = True
# Frame-based capture: cluster consecutive per-frame detections into
# separate documents when no contour is seen for this many seconds.
event_gap_s: float = 2.0
# Require at least this many sampled frames with a detection before
# committing a capture event (filters single-frame noise).
event_min_samples: int = 2
@dataclass
@@ -65,6 +95,17 @@ class OcrConfig:
min_word_confidence: float = 10.0
@dataclass
class LlmConfig:
# Use a local Ollama vision model to identify/name documents and flag
# quality issues. Falls back to Tesseract heuristics when Ollama is
# down or the model errors. ~20-40s per document vs <2s for Tesseract.
enabled: bool = True
model: str = "qwen2.5vl"
base_url: str = "http://localhost:11434"
timeout_s: float = 180.0
@dataclass
class NamingConfig:
max_vendor_len: int = 40
@@ -84,6 +125,7 @@ class Config:
document: DocumentConfig = field(default_factory=DocumentConfig)
speech: SpeechConfig = field(default_factory=SpeechConfig)
ocr: OcrConfig = field(default_factory=OcrConfig)
llm: LlmConfig = field(default_factory=LlmConfig)
naming: NamingConfig = field(default_factory=NamingConfig)
output: OutputConfig = field(default_factory=OutputConfig)