Módulo 3: Comprensión de Documentos
3. OCR + LLM
Descripción
Cuando un documento no tiene texto seleccionable (escaneado, fotografía, imagen), necesitas OCR (Optical Character Recognition). Hay dos enfoques principales: OCR tradicional (Tesseract) y Vision APIs (GPT-4 Vision, Claude 3). En esta cápsula aprenderás cuándo usar cada uno, cómo preprocesar imágenes para maximizar la calidad, y cómo combinar OCR con LLMs para extracción inteligente.
Por qué importa: La decisión OCR vs Vision afecta costo, calidad y latencia de tu pipeline documental. Tesseract es gratis, offline y rápido para volumen alto. Vision APIs ofrecen mejor calidad en documentos complejos pero tienen costo por uso. El enfoque híbrido (Tesseract + LLM para limpieza) es la opción más eficiente en muchos escenarios reales.
Conexión con el módulo: Esta cápsula se conecta con la cápsula 02 (PDFs) para documentos escaneados, con la 04 (imágenes de documentos) para fotografías, y con la 05 (extracción estructurada) donde el texto extraído se convierte en datos con schema. El Document Extractor (cápsula 08) usa estas técnicas como capa de entrada.
Conceptos Clave
OCR tradicional (Tesseract)
Tesseract es el motor OCR open-source más usado. Google lo mantiene desde 2006 y la versión 4+ usa redes LSTM.
- Pros: Gratis, local, sin límites de API, rápido, funciona offline
- Contras: Calidad variable en tablas, formularios, manuscritos; requiere preprocessing
- Cuándo: Texto impreso claro, alto volumen, presupuesto limitado, requisitos de privacidad
Vision APIs (GPT-4V, Claude 3)
Los modelos de visión "leen" la imagen completa con comprensión semántica — entienden estructura, layout y contexto.
- Pros: Alta calidad en tablas, formularios, layouts complejos; multi-idioma nativo
- Contras: Costo por token, latencia de API, límites de rate, requieren conexión
- Cuándo: Documentos con tablas/formularios, extracción estructurada, calidad crítica
Pipeline híbrido
Tesseract para extracción rápida + LLM de texto para limpiar y estructurar. Más barato que Vision, mejor que Tesseract solo.
- Pros: Costo reducido vs Vision pura, mejor calidad que Tesseract solo
- Contras: Dos pasos, latencia agregada del LLM
- Cuándo: Volumen medio-alto donde Vision es muy caro pero Tesseract no alcanza
OCR con Tesseract
Instalación y paquetes de idioma
# Instalación del motor Tesseract
# macOS: brew install tesseract
# Ubuntu: sudo apt-get install tesseract-ocr
# Windows: https://github.com/UB-Mannheim/tesseract/wiki
# Paquetes de idioma (español)
# macOS: brew install tesseract-lang
# Ubuntu: sudo apt-get install tesseract-ocr-spa
# Wrapper de Python
# pip install pytesseract Pillow
import pytesseract
langs_disponibles = pytesseract.get_languages()
print(f"Idiomas instalados: {langs_disponibles}")
Uso básico
import pytesseract
from PIL import Image
import io
def ocr_with_tesseract(image_path: str, lang: str = "spa") -> str:
"""Extrae texto de una imagen usando Tesseract."""
img = Image.open(image_path)
text = pytesseract.image_to_string(img, lang=lang)
return text.strip()
def ocr_from_bytes(image_bytes: bytes, lang: str = "spa") -> str:
"""Extrae texto desde bytes de imagen en memoria."""
img = Image.open(io.BytesIO(image_bytes))
return pytesseract.image_to_string(img, lang=lang).strip()
Modos PSM (Page Segmentation Mode)
El parámetro PSM controla cómo Tesseract segmenta la página. Elegir el modo correcto mejora drásticamente la calidad.
# 3 = Segmentación automática completa (DEFAULT)
# 4 = Asume una sola columna de texto
# 6 = Asume un bloque uniforme de texto
# 7 = Tratar imagen como una sola línea de texto
# 11 = Texto sparse sin orden particular
# 13 = Línea raw — sin preprocesamiento interno
def ocr_single_column(image_path: str, lang: str = "spa") -> str:
"""OCR optimizado para documentos de una sola columna."""
img = Image.open(image_path)
return pytesseract.image_to_string(img, lang=lang, config="--oem 3 --psm 4").strip()
def ocr_text_block(image_path: str, lang: str = "spa") -> str:
"""OCR optimizado para un bloque uniforme de texto."""
img = Image.open(image_path)
return pytesseract.image_to_string(img, lang=lang, config="--oem 3 --psm 6").strip()
def ocr_single_line(image_path: str, lang: str = "spa") -> str:
"""OCR para una sola línea (número de factura, fecha)."""
img = Image.open(image_path)
return pytesseract.image_to_string(img, lang=lang, config="--oem 3 --psm 7").strip()
Salida HOCR y coordenadas
HOCR es un formato que incluye las coordenadas de cada palabra. Útil para reconstruir layout o marcar regiones.
def ocr_word_boxes(image_path: str, lang: str = "spa") -> list[dict]:
"""Extrae cada palabra con su bounding box y confianza."""
img = Image.open(image_path)
data = pytesseract.image_to_data(img, lang=lang, output_type=pytesseract.Output.DICT)
words = []
for i in range(len(data["text"])):
if data["text"][i].strip():
words.append({
"text": data["text"][i],
"x": data["left"][i],
"y": data["top"][i],
"w": data["width"][i],
"h": data["height"][i],
"confidence": data["conf"][i],
})
return words
Preprocessing para Mejor OCR
La calidad del OCR con Tesseract depende directamente de la calidad de la imagen. Un pipeline de preprocessing transforma la imagen para maximizar la precisión.
Pipeline completo con Pillow
from PIL import Image, ImageEnhance, ImageFilter
def preprocess_for_ocr(
image_path: str,
binarize: bool = True,
denoise: bool = True,
) -> Image.Image:
"""
Pipeline: escala de grises → resize → contraste → binarización → denoising.
"""
img = Image.open(image_path)
img = img.convert("L")
width, height = img.size
if width < 1000:
scale = 2.0
img = img.resize((int(width * scale), int(height * scale)), Image.LANCZOS)
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(2.0)
if binarize:
img = img.point(lambda p: 255 if p > 128 else 0, "1")
img = img.convert("L")
if denoise:
img = img.filter(ImageFilter.MedianFilter(size=3))
return img
Comparar calidad antes y después
def compare_preprocessing(image_path: str) -> dict:
"""Compara OCR con y sin preprocessing."""
img_raw = Image.open(image_path)
img_processed = preprocess_for_ocr(image_path)
text_raw = pytesseract.image_to_string(img_raw, lang="spa").strip()
text_processed = pytesseract.image_to_string(img_processed, lang="spa").strip()
return {
"raw_chars": len(text_raw),
"processed_chars": len(text_processed),
"raw_preview": text_raw[:200],
"processed_preview": text_processed[:200],
}
resultado = compare_preprocessing("factura_baja_calidad.jpg")
print(f"Sin preprocessing: {resultado['raw_chars']} caracteres")
print(f"Con preprocessing: {resultado['processed_chars']} caracteres")
OCR con Vision API
OpenAI (GPT-4o)
from openai import OpenAI
import base64
client = OpenAI()
def ocr_with_vision(image_path: str, prompt: str = None) -> str:
"""Extrae texto de una imagen usando GPT-4 Vision."""
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
default_prompt = """Extrae TODO el texto visible en esta imagen de documento.
Mantén la estructura original: párrafos, listas, tablas.
Si hay tablas, represéntalas en formato markdown.
No inventes ni interpretes — solo transcribe lo que ves.
Si algo es ilegible, marca [ilegible]."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": prompt or default_prompt},
{"type": "image_url", "image_url": {
"url": f"data:image/jpeg;base64,{b64}",
"detail": "high",
}}
]
}],
max_tokens=4000,
)
return response.choices[0].message.content
Prompts especializados por tipo de documento
OCR_PROMPTS = {
"factura": """Extrae el texto de esta factura. Identifica y preserva:
- Encabezado (empresa, RFC, dirección)
- Datos del receptor
- Tabla de conceptos (descripción, cantidad, precio unitario, importe)
- Totales (subtotal, IVA, total)
- Folio y fecha
Usa formato markdown para tablas.""",
"formulario": """Extrae los campos de este formulario.
Para cada campo: nombre_campo: valor
Checkboxes: [X] marcado o [ ] vacío.
Campos vacíos: campo: [vacío].""",
"tabla": """Extrae la tabla en formato markdown estricto con |.
Preserva todos los valores numéricos exactos.
No redondees ni modifiques cifras.""",
}
def ocr_vision_typed(image_path: str, doc_type: str) -> str:
"""OCR con Vision API usando prompt especializado por tipo."""
prompt = OCR_PROMPTS.get(doc_type)
if not prompt:
raise ValueError(f"Tipo no soportado. Usa: {list(OCR_PROMPTS.keys())}")
return ocr_with_vision(image_path, prompt=prompt)
Anthropic (Claude 3)
import anthropic
def ocr_with_claude(image_path: str, prompt: str = None) -> str:
"""Extrae texto usando Claude 3 Vision."""
import mimetypes
mime_type = mimetypes.guess_type(image_path)[0] or "image/jpeg"
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
ac = anthropic.Anthropic()
response = ac.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4000,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {
"type": "base64", "media_type": mime_type, "data": b64,
}},
{"type": "text", "text": prompt or "Transcribe todo el texto visible. Usa markdown para tablas."},
]
}],
)
return response.content[0].text
Comparación Detallada: Tesseract vs Vision API
| Criterio | Tesseract | Vision API (GPT-4o / Claude 3) |
|---|---|---|
| Costo por imagen | Gratis | ~$0.01–0.04 |
| Precisión texto impreso | 85-95% en docs limpios | 95-99% |
| Precisión texto manuscrito | Muy baja (< 50%) | Alta (80-95%) |
| Manejo de tablas | Pierde estructura | Excelente — preserva layout |
| Formularios con checkboxes | No detecta | Detecta estado marcado/vacío |
| Velocidad | < 1 segundo local | 2-8 segundos |
| Funciona offline | Sí | No |
| Privacidad | Todo local | Datos viajan al cloud |
| Idiomas | 100+ con traineddata | Automático, multi-idioma |
| Layout multi-columna | Mezcla columnas | Respeta orden de lectura |
| Complejidad de setup | Motor + paquetes idioma | Solo API key |
| Batch / volumen | Ilimitado | Rate limits (RPM, TPM) |
| Preservación de formato | Solo texto plano | Markdown, JSON, libre |
| Docs degradados | Muy sensible a calidad | Robusto ante ruido |
Resumen práctico:
- Tesseract → alto volumen, bajo presupuesto, texto impreso limpio, privacidad, offline.
- Vision API → calidad crítica, tablas, formularios, manuscritos, documentos complejos.
- Híbrido → volumen medio, optimizar costo vs calidad.
Pipeline Híbrido: Tesseract + LLM
Tesseract extrae texto bruto (gratis y rápido), después un LLM de texto limpia y estructura el resultado. Significativamente más barato que Vision porque tokens de texto cuestan mucho menos que tokens de imagen.
def hybrid_ocr(image_path: str, lang: str = "spa") -> str:
"""Tesseract extrae texto bruto, LLM lo limpia y corrige."""
img = preprocess_for_ocr(image_path)
raw_text = pytesseract.image_to_string(img, lang=lang).strip()
if len(raw_text) < 10:
return ocr_with_vision(image_path)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": """Eres un corrector de texto OCR.
Corrige errores evidentes (letras confundidas, espacios mal puestos).
Preserva la estructura. NO inventes contenido.
Si hay texto irrecuperable, marca [ilegible]."""},
{"role": "user", "content": f"Texto OCR a corregir:\n\n{raw_text}"},
],
temperature=0,
)
return response.choices[0].message.content
Extracción estructurada con pipeline híbrido
import json
def hybrid_structured_extraction(
image_path: str,
schema: dict[str, str],
lang: str = "spa",
) -> dict:
"""Tesseract extrae texto → LLM estructura según schema. ~10x más barato que Vision."""
img = preprocess_for_ocr(image_path)
raw_text = pytesseract.image_to_string(img, lang=lang).strip()
schema_desc = "\n".join(f"- {k}: {v}" for k, v in schema.items())
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Extraes datos estructurados de texto OCR."},
{"role": "user", "content": f"""Extrae estos campos del texto OCR:
{schema_desc}
Texto OCR:
{raw_text}
Responde ÚNICAMENTE con JSON válido. Usa null si no encuentras un campo."""},
],
response_format={"type": "json_object"},
temperature=0,
)
return json.loads(response.choices[0].message.content)
Comparación de costos
Costo estimado por documento:
┌───────────────────────┬────────────────┬───────────────┐
│ Método │ Costo aprox. │ Calidad │
├───────────────────────┼────────────────┼───────────────┤
│ Tesseract solo │ $0.00 │ ★★★☆☆ │
│ Híbrido (Tess + Mini) │ $0.001–0.005 │ ★★★★☆ │
│ Vision API directa │ $0.01–0.04 │ ★★★★★ │
└───────────────────────┴────────────────┴───────────────┘
Para 10,000 documentos/mes:
Tesseract solo: $0
Híbrido: $10–50
Vision directa: $100–400
Árbol de Decisión: ¿Qué Método Usar?
¿El documento tiene texto seleccionable (PDF digital)?
├── SÍ → Usa PyMuPDF (cápsula 02). No necesitas OCR.
│
└── NO → Es imagen o PDF escaneado.
│
├── ¿Tienes presupuesto para Vision API?
│ ├── NO → Tesseract
│ │ ├── ¿Texto impreso y limpio? → Tesseract directo (PSM 3/6)
│ │ ├── ¿Imagen de baja calidad? → Preprocessing + Tesseract
│ │ └── ¿Necesitas datos estructurados? → Híbrido (Tess + LLM)
│ │
│ └── SÍ → ¿Cuántos documentos/día?
│ ├── < 100 → Vision API directa (máxima calidad)
│ ├── 100–1000 → Híbrido mayoría + Vision docs complejos
│ └── > 1000 → Tesseract + LLM + Vision como fallback
│
└── ¿Tiene tablas, formularios o manuscritos?
├── SÍ → Vision API o híbrido con prompt especializado
└── NO → Tesseract con preprocessing suele bastar
OCR de Documentos Complejos
Tablas
Tesseract pierde estructura de columnas. Usa image_to_data para coordenadas y reconstruye por posición.
import pandas as pd
def ocr_table_from_image(image_path: str, lang: str = "spa") -> pd.DataFrame:
"""Reconstruye tabla desde OCR con coordenadas (agrupando por fila Y y columna X)."""
img = preprocess_for_ocr(image_path, binarize=True)
data = pytesseract.image_to_data(img, lang=lang, output_type=pytesseract.Output.DICT)
words = []
for i in range(len(data["text"])):
text = data["text"][i].strip()
if text and int(data["conf"][i]) > 30:
words.append({"text": text, "x": data["left"][i], "y": data["top"][i]})
if not words:
return pd.DataFrame()
rows, current_row = [], [words[0]]
for word in words[1:]:
if abs(word["y"] - current_row[0]["y"]) < 15:
current_row.append(word)
else:
rows.append(sorted(current_row, key=lambda w: w["x"]))
current_row = [word]
rows.append(sorted(current_row, key=lambda w: w["x"]))
table_data = [[w["text"] for w in row] for row in rows]
max_cols = max(len(r) for r in table_data)
for row in table_data:
row.extend([""] * (max_cols - len(row)))
return pd.DataFrame(table_data[1:], columns=table_data[0] if table_data else None)
Layouts multi-columna
Divide la imagen en columnas y aplica OCR por separado para preservar orden de lectura.
def ocr_multicolumn(image_path: str, num_columns: int = 2) -> list[str]:
"""OCR columna por columna para documentos tipo periódico/revista."""
img = Image.open(image_path)
width, height = img.size
col_width = width // num_columns
columns_text = []
for i in range(num_columns):
left = i * col_width
right = (i + 1) * col_width if i < num_columns - 1 else width
col_img = img.crop((left, 0, right, height))
text = pytesseract.image_to_string(col_img, lang="spa", config="--psm 4")
columns_text.append(text.strip())
return columns_text
Formularios y texto manuscrito
def ocr_form_with_vision(image_path: str) -> str:
"""Para formularios con checkboxes, Vision API es la opción más confiable."""
return ocr_with_vision(image_path, prompt=OCR_PROMPTS["formulario"])
def ocr_handwritten(image_path: str) -> dict:
"""Para manuscritos, siempre Vision API. Retorna texto + confianza."""
prompt = """Esta imagen contiene texto escrito a mano.
Transcribe todo lo que puedas leer. Si algo es ilegible, marca [ilegible].
Al final, indica tu confianza: ALTO, MEDIO o BAJO."""
result = ocr_with_vision(image_path, prompt=prompt)
confidence = "BAJO"
for level in ["ALTO", "MEDIO", "BAJO"]:
if level in result.upper():
confidence = level
break
return {"text": result, "confidence": confidence}
Ejercicios
Ejercicio 1: Pipeline de preprocessing configurable
Crea una función preprocess_pipeline que reciba una imagen y una lista de pasos (["grayscale", "contrast", "binarize", "denoise", "resize"]). Cada paso es opcional. Después aplica OCR con Tesseract y retorna el texto.
Ver solución
def preprocess_pipeline(
image_path: str,
steps: list[str],
contrast_factor: float = 2.0,
threshold: int = 128,
) -> str:
"""Pipeline configurable de preprocessing + OCR."""
img = Image.open(image_path)
for step in steps:
if step == "grayscale":
img = img.convert("L")
elif step == "contrast":
if img.mode != "L":
img = img.convert("L")
img = ImageEnhance.Contrast(img).enhance(contrast_factor)
elif step == "binarize":
if img.mode != "L":
img = img.convert("L")
img = img.point(lambda p: 255 if p > threshold else 0, "1").convert("L")
elif step == "denoise":
img = img.filter(ImageFilter.MedianFilter(size=3))
elif step == "resize":
w, h = img.size
img = img.resize((w * 2, h * 2), Image.LANCZOS)
return pytesseract.image_to_string(img, lang="spa").strip()
texto = preprocess_pipeline(
"factura_borrosa.jpg",
steps=["grayscale", "resize", "contrast", "binarize", "denoise"],
)
Ejercicio 2: OCR híbrido con fallback inteligente
Implementa una función que: (1) intente Tesseract, (2) evalúe la calidad del resultado (proporción de caracteres alfabéticos vs basura), (3) si la calidad es baja, reintente con Vision API. Retorna resultado y método usado.
Ver solución
def evaluate_ocr_quality(text: str) -> float:
"""Puntúa calidad de texto OCR entre 0.0 y 1.0."""
if not text:
return 0.0
alpha_ratio = sum(c.isalpha() or c.isspace() for c in text) / len(text)
has_words = len(text.split()) > 3
no_garbage = not any(c in text for c in ["\x00", "□", "■"])
return min(alpha_ratio * 0.6 + (0.2 if has_words else 0) + (0.2 if no_garbage else 0), 1.0)
def ocr_smart_fallback(image_path: str, quality_threshold: float = 0.5) -> dict:
"""OCR con fallback inteligente basado en calidad."""
img = preprocess_for_ocr(image_path)
tesseract_text = pytesseract.image_to_string(img, lang="spa").strip()
quality = evaluate_ocr_quality(tesseract_text)
if quality >= quality_threshold:
return {"text": tesseract_text, "method": "tesseract", "quality": quality}
vision_text = ocr_with_vision(image_path)
return {"text": vision_text, "method": "vision_fallback", "quality": quality}
Ejercicio 3: Batch OCR con reporte de costo
Procesa una lista de imágenes usando el pipeline híbrido. Lleva la cuenta de cuántas usaron Tesseract solo vs cuántas necesitaron LLM/Vision. Genera un reporte de costo estimado.
Ver solución
def batch_ocr_with_report(
image_paths: list[str],
quality_threshold: float = 0.5,
cost_per_vision: float = 0.02,
cost_per_llm: float = 0.003,
) -> dict:
"""Batch OCR con reporte de costos."""
results = []
stats = {"tesseract_only": 0, "hybrid": 0, "vision_fallback": 0}
for path in image_paths:
img = preprocess_for_ocr(path)
raw = pytesseract.image_to_string(img, lang="spa").strip()
quality = evaluate_ocr_quality(raw)
if quality >= quality_threshold:
results.append({"path": path, "text": raw, "method": "tesseract"})
stats["tesseract_only"] += 1
elif quality >= 0.3:
cleaned = hybrid_ocr(path)
results.append({"path": path, "text": cleaned, "method": "hybrid"})
stats["hybrid"] += 1
else:
vision = ocr_with_vision(path)
results.append({"path": path, "text": vision, "method": "vision"})
stats["vision_fallback"] += 1
total_cost = stats["hybrid"] * cost_per_llm + stats["vision_fallback"] * cost_per_vision
return {
"results": results,
"stats": stats,
"total_cost_usd": round(total_cost, 4),
"cost_if_all_vision": round(len(image_paths) * cost_per_vision, 4),
}
Ejercicio 4: Extractor de tabla con Vision + validación
Usa Vision API para extraer una tabla como CSV. Conviértela a DataFrame de pandas. Valida que las columnas numéricas contengan números reales.
Ver solución
import io
def extract_table_vision(image_path: str) -> pd.DataFrame:
"""Extrae tabla de imagen con Vision API → DataFrame con validación numérica."""
prompt = """Extrae la tabla en formato CSV.
Primera línea = encabezados. Coma como separador.
No añadas texto antes ni después. Preserva valores numéricos exactos."""
csv_text = ocr_with_vision(image_path, prompt=prompt).strip()
if csv_text.startswith("```"):
lines = csv_text.split("\n")
csv_text = "\n".join(lines[1:-1])
df = pd.read_csv(io.StringIO(csv_text))
for col in df.columns:
try:
df[col] = pd.to_numeric(df[col])
except (ValueError, TypeError):
pass
numeric_cols = df.select_dtypes(include=["number"]).columns
print(f"Tabla: {len(df)} filas, {len(df.columns)} cols ({len(numeric_cols)} numéricas)")
return df
Troubleshooting
Problema: Tesseract no encuentra idioma "spa"
Causa: Datos de idioma no instalados.
Solución: brew install tesseract-lang (macOS) o sudo apt-get install tesseract-ocr-spa (Ubuntu). Verificar con tesseract --list-langs. Alternativa manual: descargar spa.traineddata de tesseract-ocr/tessdata.
Problema: Vision API devuelve texto inventado
Causa: Prompt no suficientemente restrictivo o imagen ambigua.
Solución: Usar prompt defensivo con temperatura 0:
prompt_defensivo = """Transcribe ÚNICAMENTE el texto visible.
NO interpretes, NO completes, NO inventes.
Si algo es ilegible: [ilegible]. Si no hay texto: SIN TEXTO VISIBLE."""
Problema: Tesseract devuelve basura en imágenes de baja calidad
Causa: Bajo contraste, ruido, resolución insuficiente.
Solución: Aplicar pipeline de preprocessing. Asegurar ancho > 1000px (~300 DPI para documento carta). Si el preprocessing no ayuda, usar pipeline híbrido.
Problema: OCR de tabla pierde estructura de columnas
Causa: Tesseract trata la tabla como texto continuo.
Solución: Usar --psm 6 para bloques, o dividir la imagen en celdas individuales. Para tablas complejas, usar Vision API con OCR_PROMPTS["tabla"].
Problema: PDF escaneado con muchas páginas es costoso
Causa: Cada página con Vision escala linealmente en costo.
Solución: Pipeline selectivo — Tesseract primero, Vision solo para páginas que fallan:
def smart_pdf_ocr(page_images: list[Image.Image]) -> list[str]:
"""Tesseract primero, Vision como fallback por página."""
results = []
for i, img in enumerate(page_images):
text = pytesseract.image_to_string(img, lang="spa").strip()
if evaluate_ocr_quality(text) < 0.4:
img.save(f"/tmp/page_{i}.jpg")
text = ocr_with_vision(f"/tmp/page_{i}.jpg")
results.append(text)
return results
Problema: Timeout o rate limit con Vision API en batch
Causa: Demasiadas requests simultáneas.
Solución: Retry con backoff exponencial:
import time
def ocr_with_retry(image_path: str, max_retries: int = 3) -> str:
"""Vision OCR con retry y backoff exponencial."""
for attempt in range(max_retries):
try:
return ocr_with_vision(image_path)
except Exception as e:
if "rate_limit" in str(e).lower() or "429" in str(e):
wait = 2 ** attempt
time.sleep(wait)
else:
raise
raise RuntimeError(f"Falló después de {max_retries} intentos")
Recursos adicionales
- Tesseract Documentation
- pytesseract — Python wrapper
- OpenAI Vision Guide
- Anthropic Vision (Claude)
- Pillow ImageEnhance
- Tesseract — Improve Quality