diff --git a/docker-compose.yml b/docker-compose.yml index c6648a5..544d973 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -191,6 +191,8 @@ services: REDIS_HOST: mvp-redis REDIS_PORT: 6379 REDIS_DB: 1 + volumes: + - vin-debug:/tmp/vin-debug networks: - backend - database @@ -396,3 +398,5 @@ volumes: name: mvp_loki_data mvp_grafana_data: name: mvp_grafana_data + vin-debug: + name: mvp_vin_debug diff --git a/ocr/app/extractors/vin_extractor.py b/ocr/app/extractors/vin_extractor.py index 1b310f2..9d58501 100644 --- a/ocr/app/extractors/vin_extractor.py +++ b/ocr/app/extractors/vin_extractor.py @@ -1,8 +1,10 @@ """VIN-specific OCR extractor with preprocessing and validation.""" import io import logging +import os import time from dataclasses import dataclass, field +from datetime import datetime from typing import Optional import magic @@ -57,9 +59,31 @@ class VinExtractor(BaseExtractor): # VIN character whitelist for Tesseract VIN_WHITELIST = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" + # Fixed debug output directory (inside container) + DEBUG_DIR = "/tmp/vin-debug" + def __init__(self) -> None: """Initialize VIN extractor.""" pytesseract.pytesseract.tesseract_cmd = settings.tesseract_cmd + self._debug = settings.log_level.upper() == "DEBUG" + + def _save_debug_image(self, session_dir: str, name: str, data: bytes) -> None: + """Save image bytes to the debug session directory when LOG_LEVEL=debug.""" + if not self._debug: + return + path = os.path.join(session_dir, name) + with open(path, "wb") as f: + f.write(data) + logger.debug("Saved debug image: %s (%d bytes)", name, len(data)) + + def _create_debug_session(self) -> Optional[str]: + """Create a timestamped debug directory. Returns path or None.""" + if not self._debug: + return None + ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + session_dir = os.path.join(self.DEBUG_DIR, ts) + os.makedirs(session_dir, exist_ok=True) + return session_dir def extract( self, image_bytes: bytes, content_type: Optional[str] = None @@ -89,10 +113,14 @@ class VinExtractor(BaseExtractor): ) try: + debug_session = self._create_debug_session() + logger.debug( "VIN extraction input: %d bytes, content_type=%s", len(image_bytes), content_type, ) + if debug_session: + self._save_debug_image(debug_session, "01_original.jpg", image_bytes) # Apply VIN-optimized preprocessing preprocessing_result = vin_preprocessor.preprocess(image_bytes) @@ -100,6 +128,10 @@ class VinExtractor(BaseExtractor): logger.debug( "Preprocessing steps: %s", preprocessing_result.preprocessing_applied ) + if debug_session: + self._save_debug_image( + debug_session, "02_preprocessed_adaptive.png", preprocessed_bytes + ) # Perform OCR with VIN-optimized settings raw_text, word_confidences = self._perform_ocr(preprocessed_bytes) @@ -121,6 +153,11 @@ class VinExtractor(BaseExtractor): "Otsu preprocessing steps: %s", otsu_result.preprocessing_applied, ) + if debug_session: + self._save_debug_image( + debug_session, "03_preprocessed_otsu.png", + otsu_result.image_bytes, + ) raw_text, word_confidences = self._perform_ocr(otsu_result.image_bytes) logger.debug("Otsu PSM 6 raw text: '%s'", raw_text)