# dollar_correction.py # Proceso independiente para corrección de confusión $ vs 8 import re from typing import Dict, List class DollarSignCorrectionProcessor: """ Proceso independiente para corregir confusiones del OCR entre $ y 8. Similar al proceso multilinea, puede ser aplicado a cualquier proveedor. """ def __init__(self, config: Dict = None): """ Args: config: Configuración del procesador - aggressive: bool - Si True, aplica correcciones más agresivas - context_aware: bool - Si True, usa contexto para decidir correcciones - min_confidence: float - Confianza mínima para aplicar corrección """ self.config = config or {} self.aggressive = self.config.get("aggressive", False) self.context_aware = self.config.get("context_aware", True) self.min_confidence = self.config.get("min_confidence", 0.7) def process(self, text_blocks: List[Dict]) -> List[Dict]: """ Procesa los bloques de texto y corrige confusiones entre $ y 8. Args: text_blocks: Lista de bloques de texto del OCR Returns: Lista de bloques de texto corregidos """ corrected_blocks = [] corrections_made = 0 for block in text_blocks: original_text = block['text'] corrected_text = self._correct_text(original_text, block) if corrected_text != original_text: corrections_made += 1 print(f"DEBUG: Corrección $ vs 8: '{original_text}' -> '{corrected_text}'") # Crear nuevo bloque con texto corregido corrected_block = block.copy() corrected_block['text'] = corrected_text corrected_block['was_corrected'] = True corrected_block['original_text'] = original_text corrected_blocks.append(corrected_block) else: corrected_blocks.append(block) print(f"INFO: Correcciones $ vs 8 aplicadas: {corrections_made} de {len(text_blocks)} bloques") return corrected_blocks def _correct_text(self, text: str, block: Dict) -> str: """ Aplica correcciones al texto basándose en patrones y contexto. Args: text: Texto a corregir block: Bloque de texto con metadata (posición, confianza, etc.) Returns: Texto corregido """ corrected = text # Patrón 1: "8" seguido de números (probablemente es "$") # Ejemplo: "8 12.99" -> "$ 12.99" # Ejemplo: "812.99" -> "$12.99" corrected = re.sub( r'\b8\s*(\d+\.?\d*)\b', lambda m: f"$ {m.group(1)}" if self._is_likely_price(m.group(1)) else m.group(0), corrected ) # Patrón 2: "8" al inicio de línea seguido de espacio y números # Ejemplo: "8 Total" -> "$ Total" if self.context_aware: corrected = re.sub( r'^8\s+(Total|Subtotal|HST|Tax|Amount|Price)', r'$ \1', corrected, flags=re.IGNORECASE ) # Patrón 3: "8" en contexto de moneda (después de palabras clave) # Ejemplo: "Total 8 123.45" -> "Total $ 123.45" corrected = re.sub( r'(Total|Subtotal|HST|Tax|Amount|Price|Cost)\s+8\s*(\d+\.?\d*)', r'\1 $ \2', corrected, flags=re.IGNORECASE ) # Patrón 4: Múltiples "8" en secuencia (probablemente "$") # Ejemplo: "88" -> "$$" (raro pero posible) if self.aggressive: corrected = re.sub(r'88', '$$', corrected) # Patrón 5: "8" entre espacios y números decimales # Ejemplo: "Item 8 12.99 8 24.98" -> "Item $ 12.99 $ 24.98" corrected = re.sub( r'\s8\s+(\d+\.\d{2})\b', r' $ \1', corrected ) # Patrón 6: "8" al final de palabra seguido de números # Ejemplo: "Price8123.45" -> "Price$123.45" corrected = re.sub( r'([a-zA-Z])8(\d+\.?\d*)', lambda m: f"{m.group(1)}${m.group(2)}" if self._is_likely_price(m.group(2)) else m.group(0), corrected ) # Patrón 7: "8" solo seguido de espacio y dígitos con decimales # Ejemplo: "8 1.99" -> "$ 1.99" corrected = re.sub( r'\b8\s+(\d+\.\d{2})\b', r'$ \1', corrected ) # Patrón 8: Líneas que empiezan con "8" y tienen formato de precio # Ejemplo: "8123.45" -> "$123.45" corrected = re.sub( r'^8(\d+\.\d{2})\b', r'$\1', corrected, flags=re.MULTILINE ) return corrected def _is_likely_price(self, number_str: str) -> bool: """ Determina si un número es probablemente un precio. Args: number_str: String con el número Returns: True si parece un precio """ try: value = float(number_str) # Precios típicos: entre 0.01 y 10000 if value < 0.01 or value > 10000: return False # Si tiene 2 decimales, muy probable que sea precio if '.' in number_str and len(number_str.split('.')[1]) == 2: return True # Si es un número redondo pequeño, menos probable if value < 10 and '.' not in number_str: return False return True except ValueError: return False