9.4 KiB
9.4 KiB
Plan: OCR Inteligent cu Early Exit
Context Handover Document - Plan de implementare pentru următoarea sesiune
Obiectiv
Optimizare proces OCR - dacă PaddleOCR pe light preprocessing dă rezultate bune, să NU mai ruleze heavy preprocessing și Tesseract.
Criterii Early Exit (TOATE trebuie îndeplinite)
Continuă cu alte încercări DACĂ:
- Confidență < 85% SAU
- Lipsește ORICARE din câmpurile critice:
- ✗ Număr bon (
receipt_number) - ✗ Dată (
receipt_date) - ✗ Valoare totală (
amount) - ✗ Valoare TVA (
tva_totalsautva_entries) - ✗ Cod fiscal (
cui)
- ✗ Număr bon (
Early exit DOAR când:
- Confidență >= 85% ȘI
- TOATE 5 câmpurile sunt extrase
Flow Propus: Adaptive OCR Pipeline
1. PaddleOCR + Light Preprocessing (cel mai rapid, cel mai bun pentru PDF-uri clare)
↓
Verifică: conf >= 85% AND toate 5 câmpurile extrase?
├─ DA → STOP, returnează rezultat
└─ NU → continuă la pasul 2
2. PaddleOCR + Heavy Preprocessing (pentru bonuri termice șterse)
↓
Combină cu rezultatul anterior (merge)
Verifică: toate câmpurile extrase acum?
├─ DA → STOP
└─ NU → continuă la pasul 3
3. Tesseract + Light (fallback pentru cazuri dificile)
↓
Combină toate rezultatele
Returnează cel mai bun rezultat combinat
Beneficii Estimate
| Tip document | OCR calls | Timp estimat |
|---|---|---|
| PDF clar (Kineterra) | 1 | ~2-3 sec |
| PDF mediu | 2 | ~5 sec |
| Bon termic șters | 3 | ~8-10 sec |
Comparație cu acum: Totdeauna 4 calls → maxim 3, de obicei 1-2
Fișier de Modificat
data-entry-app/backend/app/services/ocr_service.py
Înlocuire completă _process_sync():
def _process_sync(
self,
image_path: Path,
mime_type: str
) -> Tuple[bool, str, Optional[ExtractionResult]]:
"""Synchronous processing with ADAPTIVE OCR pipeline."""
logger.info(f"[OCR Service] Starting processing: {image_path}, mime: {mime_type}")
# Load image
if mime_type == 'application/pdf':
try:
images = self.preprocessor.pdf_to_images(image_path)
if not images:
return False, "Failed to extract images from PDF", None
image = images[0]
except RuntimeError as e:
return False, str(e), None
else:
try:
image = self.preprocessor.load_image(image_path)
except ValueError as e:
return False, str(e), None
raw_texts = []
extraction = None
# ══════════════════════════════════════════════════════════════
# STEP 1: PaddleOCR + Light (fastest, best for clear PDFs)
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 1: PaddleOCR + Light preprocessing")
light_img = self.preprocessor.preprocess_light(image)
try:
paddle_light = self.ocr_engine._paddle_recognize(light_img)
if paddle_light and paddle_light.text:
extraction = self.extractor.extract(paddle_light.text)
extraction.ocr_engine = "paddle-light"
raw_texts.append(f"═══ PaddleOCR (light, conf: {paddle_light.confidence:.0%}) ═══\n{paddle_light.text}")
# Early exit if complete
if self._is_extraction_complete(extraction):
extraction.raw_text = "\n\n".join(raw_texts)
logger.info("[OCR] ✓ Early exit: complete extraction from paddle-light")
return True, "OCR complete (fast mode)", extraction
except Exception as e:
logger.warning(f"[OCR] PaddleOCR light failed: {e}")
extraction = ExtractionResult()
# ══════════════════════════════════════════════════════════════
# STEP 2: PaddleOCR + Heavy (for faded thermal receipts)
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 2: PaddleOCR + Heavy preprocessing")
heavy_img = self.preprocessor.preprocess_heavy(image)
try:
paddle_heavy = self.ocr_engine._paddle_recognize(heavy_img)
if paddle_heavy and paddle_heavy.text:
extraction_heavy = self.extractor.extract(paddle_heavy.text)
extraction_heavy.ocr_engine = "paddle-heavy"
raw_texts.append(f"═══ PaddleOCR (heavy, conf: {paddle_heavy.confidence:.0%}) ═══\n{paddle_heavy.text}")
# Merge with previous
extraction = self._merge_extractions(extraction, extraction_heavy)
if self._is_extraction_complete(extraction):
extraction.raw_text = "\n\n".join(raw_texts)
extraction.ocr_engine = "paddle-adaptive"
logger.info("[OCR] ✓ Early exit: complete extraction after paddle-heavy")
return True, "OCR complete (paddle dual)", extraction
except Exception as e:
logger.warning(f"[OCR] PaddleOCR heavy failed: {e}")
# ══════════════════════════════════════════════════════════════
# STEP 3: Tesseract fallback
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 3: Tesseract fallback")
try:
tesseract_result = self.ocr_engine._tesseract_recognize(light_img)
if tesseract_result and tesseract_result.text:
extraction_tess = self.extractor.extract(tesseract_result.text)
extraction_tess.ocr_engine = "tesseract"
raw_texts.append(f"═══ Tesseract (conf: {tesseract_result.confidence:.0%}) ═══\n{tesseract_result.text}")
extraction = self._merge_extractions(extraction, extraction_tess)
except Exception as e:
logger.warning(f"[OCR] Tesseract failed: {e}")
# Final result
if extraction is None:
return False, "No text detected", None
extraction.raw_text = "\n\n".join(raw_texts)
extraction.ocr_engine = "adaptive-full"
# Build result message
fields_found = []
if extraction.amount: fields_found.append("amount")
if extraction.receipt_date: fields_found.append("date")
if extraction.receipt_number: fields_found.append("number")
if extraction.cui: fields_found.append("CUI")
if extraction.tva_total or extraction.tva_entries: fields_found.append("TVA")
message = f"OCR complete (full pipeline). Found: {', '.join(fields_found) or 'no fields'}"
logger.info(f"[OCR] Final result: {message}")
return True, message, extraction
Adăugare metodă _is_extraction_complete():
def _is_extraction_complete(self, ext: ExtractionResult, min_confidence: float = 0.85) -> bool:
"""
Check if extraction has ALL required fields to skip further processing.
Required for early exit (ALL must be true):
- Overall confidence >= 85%
- ALL 5 critical fields present: number, date, amount, TVA, CUI
"""
# Must have high confidence
if ext.overall_confidence < min_confidence:
logger.info(f"[OCR] Confidence {ext.overall_confidence:.0%} < {min_confidence:.0%} - continuing")
return False
# Check all required fields
has_number = bool(ext.receipt_number)
has_date = bool(ext.receipt_date)
has_amount = bool(ext.amount)
has_tva = bool(ext.tva_total) or bool(ext.tva_entries)
has_cui = bool(ext.cui)
missing = []
if not has_number: missing.append("number")
if not has_date: missing.append("date")
if not has_amount: missing.append("amount")
if not has_tva: missing.append("TVA")
if not has_cui: missing.append("CUI")
if missing:
logger.info(f"[OCR] Missing: {', '.join(missing)} - continuing")
return False
logger.info(f"[OCR] ✓ All 5 fields found with {ext.overall_confidence:.0%} confidence")
return True
Cod de Șters
După implementare, poți șterge:
_merge_all_extractions()- înlocuită de flow secvențial_format_dual_raw_text()- nefolosită- Bucla
for i, processed in enumerate(variants):- înlocuită complet
Context: Rezultate OCR Kineterra
Din testele anterioare, PaddleOCR + Light a dat cele mai bune rezultate:
| Variantă | Conf | CUI | Adresa |
|---|---|---|---|
| PaddleOCR Light | 83% | 31180432 ✓ | MUN.CONSTANTA ✓ |
| PaddleOCR Heavy | 83% | 31189432 ✗ | CONSTANTN ✗ |
| Tesseract Light | 50% | 31100400 ✗ | corupt |
| Tesseract Heavy | 42% | - | corupt |
Testare
După implementare, testează cu toate PDF-urile:
abonament kineterra.pdf- ar trebui să facă early exit la Step 1benzina 27 octombrie.pdf- verifică extracție completăigiena 11 octombrie.pdf- verifică extracție completăbenzina 14 august.pdf- verifică extracție completă
Generat: 2024-12-12 Pentru continuare în următoarea sesiune Claude