Módulo 3: Comprensión de Documentos
4. Imágenes de Documentos
Descripción
Cuando un documento es un PDF escaneado o una imagen (foto de factura, captura de pantalla), necesitas convertir las páginas a imágenes y enviarlas a Vision APIs. En esta cápsula aprenderás a preparar imágenes de documentos para análisis: conversión con distintas librerías, optimización para reducir tokens y costo, envío a múltiples proveedores, y procesamiento inteligente de documentos con muchas páginas.
Por qué importa: Las Vision APIs requieren imágenes en formato específico (Base64, URL). Un PDF de 50 páginas enviado sin optimización puede costar 10x más de lo necesario. Dominar la preparación evita errores, optimiza costos y permite escalar a volúmenes reales de producción.
Conexión con el módulo: En la cápsula 02 aprendiste a extraer texto e imágenes de PDFs con PyMuPDF. En la cápsula 03 viste OCR tradicional vs Vision APIs. Aquí combinas ambos: conviertes páginas completas a imágenes y las envías a Vision para obtener comprensión total del documento — layout, tablas, imágenes y texto juntos.
Conceptos Clave
Formatos aceptados por Vision APIs
| Formato | OpenAI | Anthropic | Google Gemini | Límite típico |
|---|---|---|---|---|
| Base64 | Sí | Sí | Sí | 20MB (OpenAI) |
| URL pública | Sí | Sí | Sí | Misma URL |
| PNG | Sí | Sí | Sí | — |
| JPEG | Sí | Sí | Sí | — |
| WebP | Sí | Sí | Sí | — |
| GIF | Sí | Sí | Sí | — |
Límites por proveedor
| Proveedor | Max imágenes/request | Max tamaño | Detalle |
|---|---|---|---|
| OpenAI | 10 | 20 MB total | detail=low usa 85 tokens fijos |
| Anthropic | 5 (Claude 3.5) | 5 MB por imagen | Base64 + media_type obligatorio |
| 16 (Gemini) | 20 MB total | Acepta bytes raw en SDK |
Cómo las Vision APIs procesan imágenes
Las Vision APIs no reciben la imagen tal cual. Internamente la redimensionan y dividen en tiles. OpenAI con detail=high:
- Escala la imagen para que quepa en 2048×2048
- Escala para que el lado más corto sea 768px
- Divide en tiles de 512×512
- Cada tile = 170 tokens, más 85 tokens base
Una imagen de 1024×1024 consume ~765 tokens. Una de 4096×4096 consume lo mismo que una de 2048×2048 (por el paso 1).
Métodos de Conversión: PDF a Imágenes
Método 1: PyMuPDF (fitz) — pixmap directo
PyMuPDF genera pixmaps nativos sin dependencias externas de sistema. Es la opción más portátil.
import fitz
import io
import base64
from PIL import Image
def pdf_to_images_pymupdf(pdf_path: str, dpi: int = 150) -> list[Image.Image]:
"""Convierte PDF a lista de imágenes PIL usando PyMuPDF."""
doc = fitz.open(pdf_path)
images = []
zoom = dpi / 72
mat = fitz.Matrix(zoom, zoom)
for page in doc:
pix = page.get_pixmap(matrix=mat, alpha=False)
img = Image.open(io.BytesIO(pix.tobytes("png")))
images.append(img)
doc.close()
return images
Método 2: pdf2image (Poppler)
pdf2image usa Poppler como backend. Produce imágenes de alta calidad y ofrece opciones de renderizado avanzadas.
from pdf2image import convert_from_path
def pdf_to_images_poppler(pdf_path: str, dpi: int = 150) -> list[Image.Image]:
"""Convierte PDF a imágenes usando pdf2image (requiere Poppler)."""
return convert_from_path(
pdf_path,
dpi=dpi,
fmt="png",
thread_count=4,
grayscale=False,
size=None # None = usa DPI, o (width, height) para forzar tamaño
)
def pdf_specific_pages(pdf_path: str, first: int, last: int, dpi: int = 150) -> list[Image.Image]:
"""Convierte solo un rango de páginas (1-indexed)."""
return convert_from_path(pdf_path, dpi=dpi, first_page=first, last_page=last)
Manejo de diferentes tamaños de página
Los PDFs pueden tener páginas con tamaños mixtos (carta, oficio, A4). Es importante normalizar.
def normalize_page_sizes(images: list[Image.Image], target_width: int = 1024) -> list[Image.Image]:
"""Normaliza imágenes a un ancho consistente manteniendo proporciones."""
normalized = []
for img in images:
w, h = img.size
if w != target_width:
ratio = target_width / w
img = img.resize((target_width, int(h * ratio)), Image.Resampling.LANCZOS)
normalized.append(img)
return normalized
Comparación de métodos
| Criterio | PyMuPDF | pdf2image (Poppler) |
|---|---|---|
| Dependencia externa | Solo pip | Poppler (sistema) |
| Velocidad | Rápido | Más rápido en batch |
| Calidad | Buena | Excelente |
| Multihilo | Manual | thread_count nativo |
| Instalación | pip install pymupdf | pip install pdf2image + Poppler |
DPI y su impacto
| DPI | Resolución carta (8.5×11") | Tamaño aprox. | Uso recomendado |
|---|---|---|---|
| 72 | 612×792 px | ~100 KB | Preview rápido |
| 150 | 1275×1650 px | ~400 KB | Balance costo/calidad |
| 200 | 1700×2200 px | ~800 KB | Texto pequeño, tablas |
| 300 | 2550×3300 px | ~2 MB | Excesivo para Vision |
Recomendación: 150 DPI es el punto óptimo para Vision APIs. Más de 200 DPI no mejora la comprensión del modelo y multiplica el costo en tokens.
Optimización de Imágenes para Vision
Redimensionar para uso óptimo de tokens
OpenAI redimensiona internamente a max 2048×2048. Enviar imágenes más grandes es desperdiciar ancho de banda sin beneficio.
def optimize_for_vision(
img: Image.Image,
max_side: int = 2048,
jpeg_quality: int = 85
) -> bytes:
"""Redimensiona si excede max_side y comprime a JPEG."""
w, h = img.size
if max(w, h) > max_side:
ratio = max_side / max(w, h)
img = img.resize((int(w * ratio), int(h * ratio)), Image.Resampling.LANCZOS)
if img.mode == "RGBA":
img = img.convert("RGB")
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=jpeg_quality, optimize=True)
return buf.getvalue()
JPEG quality vs tamaño: el tradeoff
def compare_jpeg_quality(img: Image.Image) -> dict:
"""Muestra el impacto de quality en tamaño."""
if img.mode == "RGBA":
img = img.convert("RGB")
results = {}
for quality in [50, 65, 75, 85, 95]:
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality, optimize=True)
results[quality] = f"{len(buf.getvalue()) / 1024:.0f} KB"
return results
# Resultado típico para una página de documento a 150 DPI:
# {50: "80 KB", 65: "120 KB", 75: "160 KB", 85: "250 KB", 95: "600 KB"}
Hallazgo clave: Quality 75-85 es el punto óptimo. Por debajo de 65 el texto se vuelve borroso. Por encima de 90 el tamaño crece exponencialmente sin mejora perceptible.
Calcular DPI óptimo según el documento
def calculate_optimal_dpi(
page_width_inches: float,
page_height_inches: float,
target_pixels: int = 1500
) -> int:
"""Calcula el DPI que produce una imagen cercana al target en su lado mayor."""
longer_side = max(page_width_inches, page_height_inches)
ideal_dpi = int(target_pixels / longer_side)
return max(100, min(ideal_dpi, 250))
# Carta (8.5×11"): 136 → 150 | A4 (8.27×11.69"): 128 → 150 | Oficio (8.5×14"): 107
Función completa de preparación
def prepare_page_for_vision(
img: Image.Image,
max_side: int = 2048,
jpeg_quality: int = 80,
max_bytes: int = 5 * 1024 * 1024
) -> dict:
"""Prepara una imagen de página con optimización progresiva."""
img_bytes = optimize_for_vision(img, max_side, jpeg_quality)
while len(img_bytes) > max_bytes and jpeg_quality > 40:
jpeg_quality -= 10
img_bytes = optimize_for_vision(img, max_side, jpeg_quality)
if len(img_bytes) > max_bytes:
max_side = int(max_side * 0.7)
img_bytes = optimize_for_vision(img, max_side, jpeg_quality)
b64 = base64.b64encode(img_bytes).decode()
return {
"base64": b64,
"media_type": "image/jpeg",
"size_bytes": len(img_bytes),
"quality_used": jpeg_quality
}
Envío a Múltiples Proveedores
Cada proveedor espera la imagen en un formato ligeramente diferente. Referencia: Módulo 1, cápsula 06.
OpenAI: data URI
from openai import OpenAI
def send_to_openai(pages: list[dict], prompt: str, detail: str = "high") -> str:
"""Envía imágenes de páginas a OpenAI Vision."""
client = OpenAI()
content = [{"type": "text", "text": prompt}]
for page in pages[:10]:
content.append({
"type": "image_url",
"image_url": {
"url": f"data:{page['media_type']};base64,{page['base64']}",
"detail": detail
}
})
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": content}],
max_tokens=4000
)
return response.choices[0].message.content
Anthropic: Base64 + media_type explícito
import anthropic
def send_to_anthropic(pages: list[dict], prompt: str) -> str:
"""Envía imágenes de páginas a Claude Vision."""
client = anthropic.Anthropic()
content = []
for page in pages[:5]:
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": page["media_type"],
"data": page["base64"]
}
})
content.append({"type": "text", "text": prompt})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4000,
messages=[{"role": "user", "content": content}]
)
return response.content[0].text
Google Gemini: objetos PIL directos
import google.generativeai as genai
def send_to_gemini(page_images: list[Image.Image], prompt: str) -> str:
"""Envía imágenes a Gemini — acepta PIL directamente."""
model = genai.GenerativeModel("gemini-2.0-flash")
parts = [prompt] + page_images[:16]
response = model.generate_content(parts)
return response.text
Función unificada
def send_to_provider(pages: list[dict], images: list[Image.Image], prompt: str, provider: str = "openai") -> str:
providers = {
"openai": lambda: send_to_openai(pages, prompt),
"anthropic": lambda: send_to_anthropic(pages, prompt),
"gemini": lambda: send_to_gemini(images, prompt),
}
if provider not in providers:
raise ValueError(f"Proveedor no soportado: {provider}")
return providers[provider]()
Procesamiento Multi-Página
Estrategia 1: Secuencial
Procesa cada página por separado. Útil cuando cada página es independiente.
def process_sequential(images: list[Image.Image], prompt: str) -> list[str]:
"""Procesa cada página individualmente."""
results = []
for i, img in enumerate(images):
page_data = prepare_page_for_vision(img)
result = send_to_openai([page_data], f"Página {i + 1}. {prompt}")
results.append(result)
return results
Estrategia 2: Batch
Agrupa páginas en un solo request. Mejor contexto entre páginas y menos requests.
def process_batch(images: list[Image.Image], prompt: str, batch_size: int = 5) -> list[str]:
"""Procesa páginas en grupos para reducir requests."""
results = []
for i in range(0, len(images), batch_size):
batch = images[i:i + batch_size]
pages_data = [prepare_page_for_vision(img) for img in batch]
batch_prompt = f"{prompt}\n\nPáginas {i + 1} a {i + len(batch)} del documento."
result = send_to_openai(pages_data, batch_prompt)
results.append(result)
return results
Estrategia 3: Selectiva (inteligente)
Solo envía a Vision las páginas que lo necesitan. Las de solo texto se procesan con OCR local.
import numpy as np
def classify_page(img: Image.Image) -> str:
"""Clasifica página como 'text_only' o 'complex' usando variación de grises."""
small = img.resize((100, 100)).convert("L")
arr = np.array(small)
return "text_only" if len(np.unique(arr)) < 20 else "complex"
def process_selective(images: list[Image.Image], prompt: str) -> list[dict]:
"""Envía solo páginas complejas a Vision. Páginas simples → OCR local."""
import pytesseract
results = []
for i, img in enumerate(images):
page_type = classify_page(img)
if page_type == "complex":
page_data = prepare_page_for_vision(img)
text = send_to_openai([page_data], prompt)
method = "vision"
else:
text = pytesseract.image_to_string(img, lang="spa")
method = "ocr"
results.append({"page": i + 1, "type": page_type, "method": method, "text": text})
return results
Cuándo usar cada estrategia
| Estrategia | Requests API | Costo | Contexto entre páginas | Caso de uso |
|---|---|---|---|---|
| Secuencial | 1 por página | Alto | Ninguno | Páginas independientes |
| Batch | 1 por grupo | Medio | Dentro del grupo | Documentos coherentes |
| Selectiva | Solo complejas | Bajo | Ninguno | Documentos largos, presupuesto limitado |
Pipeline Completo: PDF → Imágenes → Vision → Texto
import json
from dataclasses import dataclass, field
@dataclass
class PageResult:
page_number: int
text: str
method: str
tokens_estimated: int = 0
@dataclass
class DocumentResult:
source: str
total_pages: int
pages: list[PageResult] = field(default_factory=list)
combined_text: str = ""
total_cost: float = 0.0
def estimate_image_tokens(size_bytes: int, detail: str = "high") -> int:
if detail == "low":
return 85
if size_bytes < 200_000:
return 765
elif size_bytes < 500_000:
return 1105
return 1105 + (size_bytes - 500_000) // 100_000 * 170
def full_pipeline(
pdf_path: str,
prompt: str = "Extrae todo el texto visible. Mantén la estructura.",
dpi: int = 150,
batch_size: int = 5,
detail: str = "high",
selective: bool = True
) -> DocumentResult:
"""Pipeline completo: PDF → Imágenes → Vision API → Texto combinado."""
images = pdf_to_images_pymupdf(pdf_path, dpi=dpi)
images = normalize_page_sizes(images)
doc_result = DocumentResult(source=pdf_path, total_pages=len(images))
import pytesseract
for batch_start in range(0, len(images), batch_size):
batch_imgs = images[batch_start:batch_start + batch_size]
pages_data = []
for i, img in enumerate(batch_imgs):
page_num = batch_start + i + 1
if selective and classify_page(img) == "text_only":
text = pytesseract.image_to_string(img, lang="spa").strip()
doc_result.pages.append(PageResult(page_num, text, "ocr_local"))
continue
page_data = prepare_page_for_vision(img)
page_data["page_number"] = page_num
pages_data.append(page_data)
if pages_data:
page_nums = [p["page_number"] for p in pages_data]
batch_prompt = (
f"{prompt}\n\nPáginas: {page_nums}. "
f"Separa con '--- Página N ---'."
)
result_text = send_to_openai(pages_data, batch_prompt, detail)
sections = result_text.split("--- Página")
for j, pd in enumerate(pages_data):
section = sections[j + 1] if j + 1 < len(sections) else result_text
tokens = estimate_image_tokens(pd["size_bytes"], detail)
doc_result.pages.append(PageResult(pd["page_number"], section.strip(), "vision_api", tokens))
doc_result.pages.sort(key=lambda p: p.page_number)
doc_result.combined_text = "\n\n".join(p.text for p in doc_result.pages)
doc_result.total_cost = sum(p.tokens_estimated * 0.0000025 for p in doc_result.pages)
return doc_result
Uso del pipeline
result = full_pipeline("contrato_servicio.pdf", selective=True)
print(f"Páginas: {result.total_pages} | Costo: ${result.total_cost:.4f}")
for p in result.pages:
print(f" Pág {p.page_number}: {p.method} ({p.tokens_estimated} tokens)")
Costo y Optimización
Calculando costo para documentos multi-página
def estimate_document_cost(num_pages: int, detail: str = "high", model: str = "gpt-4o") -> dict:
"""Estima el costo de procesar un documento completo."""
prices = {
"gpt-4o": {"input": 2.50, "output": 10.00},
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
}
price = prices.get(model, prices["gpt-4o"])
tokens_per_image = 85 if detail == "low" else 765
input_tokens = num_pages * tokens_per_image + 100
output_tokens = num_pages * 500
input_cost = (input_tokens / 1_000_000) * price["input"]
output_cost = (output_tokens / 1_000_000) * price["output"]
return {
"model": model, "detail": detail,
"input_tokens": input_tokens, "output_tokens": output_tokens,
"total_cost": f"${input_cost + output_cost:.4f}"
}
Costos comparativos
| Páginas | Modelo | Detail | Costo estimado |
|---|---|---|---|
| 10 | gpt-4o | high | ~$0.069 |
| 10 | gpt-4o | low | ~$0.052 |
| 10 | gpt-4o-mini | high | ~$0.004 |
| 50 | gpt-4o | high | ~$0.346 |
| 50 | gpt-4o-mini | low | ~$0.002 |
6 estrategias para reducir costo
| # | Estrategia | Ahorro estimado | Trade-off |
|---|---|---|---|
| 1 | detail=low en OpenAI | 85 tokens fijos vs ~765 | Pierde detalle fino, malo para tablas |
| 2 | gpt-4o-mini en vez de gpt-4o | ~15x más barato | Menor capacidad en layouts complejos |
| 3 | DPI bajo (100-120) | Menos bytes, más rápido | Texto pequeño puede ser ilegible |
| 4 | JPEG quality 65-75 | ~40% menos bytes | Ligera pérdida en bordes finos |
| 5 | Selectiva: OCR local para texto simple | 0 tokens para esas páginas | Necesita clasificador de página |
| 6 | Batch de páginas | Menos overhead de prompt | Límite de imágenes por request |
Función de presupuesto
def process_within_budget(pdf_path: str, prompt: str, max_budget: float = 0.10) -> DocumentResult:
"""Procesa un documento ajustando configuración para respetar presupuesto."""
images = pdf_to_images_pymupdf(pdf_path, dpi=150)
n = len(images)
configs = [
{"detail": "high", "model": "gpt-4o"},
{"detail": "low", "model": "gpt-4o"},
{"detail": "high", "model": "gpt-4o-mini"},
{"detail": "low", "model": "gpt-4o-mini"},
]
for cfg in configs:
cost = estimate_document_cost(n, cfg["detail"], cfg["model"])
total = float(cost["total_cost"].replace("$", ""))
if total <= max_budget:
print(f"Config: {cfg['model']}, detail={cfg['detail']}, costo≈${total:.4f}")
return full_pipeline(pdf_path, prompt=prompt, selective=True, detail=cfg["detail"])
print(f"Presupuesto muy bajo para {n} páginas. Usando config mínima.")
return full_pipeline(pdf_path, prompt=prompt, selective=True, detail="low")
Caso de Uso: Factura Multi-Página
Una factura de 4 páginas: portada con datos del emisor, detalle de líneas (tabla), impuestos/totales, y términos.
def process_invoice(pdf_path: str) -> dict:
"""Procesa una factura multi-página y extrae datos estructurados."""
extraction_prompt = """Analiza esta factura y extrae en JSON:
{
"emisor": {"nombre": "", "rfc": "", "direccion": ""},
"receptor": {"nombre": "", "rfc": "", "direccion": ""},
"folio": "", "fecha": "",
"lineas": [{"descripcion": "", "cantidad": 0, "precio_unitario": 0, "total": 0}],
"subtotal": 0, "iva": 0, "total": 0, "moneda": "", "metodo_pago": ""
}
Responde ÚNICAMENTE con JSON válido. Usa null para campos no encontrados.
Extrae TODAS las líneas de producto de la tabla."""
images = pdf_to_images_pymupdf(pdf_path, dpi=200)
pages_data = [prepare_page_for_vision(img) for img in images]
client = OpenAI()
content = [{"type": "text", "text": extraction_prompt}]
for page in pages_data:
content.append({
"type": "image_url",
"image_url": {"url": f"data:{page['media_type']};base64,{page['base64']}", "detail": "high"}
})
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": content}],
response_format={"type": "json_object"},
temperature=0,
max_tokens=4000
)
invoice = json.loads(response.choices[0].message.content)
if invoice.get("lineas"):
calc = sum(l.get("total", 0) for l in invoice["lineas"] if l.get("total"))
reported = invoice.get("subtotal", 0)
if reported and abs(calc - reported) > 1:
invoice["_warning"] = f"Subtotal calculado ({calc}) ≠ reportado ({reported})"
return invoice
Uso
invoice = process_invoice("factura_servicios_2024.pdf")
print(f"Emisor: {invoice['emisor']['nombre']} | Folio: {invoice['folio']}")
for line in invoice.get("lineas", []):
print(f" {line['descripcion']}: {line['cantidad']} × ${line['precio_unitario']}")
print(f"Total: ${invoice['total']} {invoice['moneda']}")
Troubleshooting
Problema 1: "Poppler not found" al usar pdf2image
Causa: pdf2image necesita Poppler instalado a nivel de sistema.
Solución:
# macOS
brew install poppler
# Ubuntu/Debian
sudo apt-get install poppler-utils
Si no puedes instalar Poppler, usa PyMuPDF como alternativa (no requiere dependencias de sistema).
Problema 2: MemoryError con PDFs grandes
Causa: Convertir todas las páginas a imágenes de alta resolución al mismo tiempo agota la memoria.
Solución: Procesar página por página con un generador.
def process_large_pdf(pdf_path: str, dpi: int = 150):
"""Procesa PDFs grandes sin agotar memoria."""
doc = fitz.open(pdf_path)
mat = fitz.Matrix(dpi / 72, dpi / 72)
for i in range(len(doc)):
pix = doc[i].get_pixmap(matrix=mat, alpha=False)
yield i + 1, Image.open(io.BytesIO(pix.tobytes("png")))
doc.close()
Problema 3: Imágenes borrosas en la conversión
Causa: DPI demasiado bajo para el contenido (tablas con texto pequeño).
Solución: Incrementar DPI a 200. Si solo ciertas áreas necesitan detalle, recortar la región:
def extract_region(img: Image.Image, bbox: tuple) -> Image.Image:
"""Recorta una región específica para enviarla en alta resolución."""
return img.crop(bbox) # bbox = (x1, y1, x2, y2)
Problema 4: Exceder límites de imágenes por request
Causa: Más de 10 imágenes a OpenAI o más de 5 a Anthropic.
Solución: Dividir en chunks respetando el límite de cada proveedor.
PROVIDER_LIMITS = {"openai": 10, "anthropic": 5, "gemini": 16}
def chunk_pages(pages: list, provider: str = "openai") -> list[list]:
limit = PROVIDER_LIMITS.get(provider, 5)
return [pages[i:i + limit] for i in range(0, len(pages), limit)]
Problema 5: Timeout en documentos con muchas páginas
Causa: Un request con muchas imágenes excede el timeout del cliente HTTP.
Solución: client = OpenAI(timeout=120.0, max_retries=3)
Ejercicios
Ejercicio 1: Comparar calidad por DPI
Escribe una función que convierta la primera página de un PDF con tres DPI distintos (100, 150, 200) y retorne el tamaño en KB de cada una en PNG y JPEG.
Ver solución
def compare_dpi_quality(pdf_path: str) -> dict:
doc = fitz.open(pdf_path)
page = doc[0]
results = {}
for dpi in [100, 150, 200]:
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat, alpha=False)
img = Image.open(io.BytesIO(pix.tobytes("png")))
buf_png = io.BytesIO()
img.save(buf_png, format="PNG")
buf_jpg = io.BytesIO()
img.convert("RGB").save(buf_jpg, format="JPEG", quality=80)
results[dpi] = {
"resolution": f"{img.size[0]}×{img.size[1]}",
"png_kb": round(len(buf_png.getvalue()) / 1024),
"jpeg_kb": round(len(buf_jpg.getvalue()) / 1024)
}
doc.close()
return results
Ejercicio 2: Enviar misma página a dos proveedores
Crea una función que tome una imagen PIL, la prepare, y la envíe a OpenAI y Anthropic con el mismo prompt. Retorna ambas respuestas para comparar.
Ver solución
def compare_providers(img: Image.Image, prompt: str) -> dict:
page_data = prepare_page_for_vision(img)
return {
"openai": send_to_openai([page_data], prompt),
"anthropic": send_to_anthropic([page_data], prompt),
}
Ejercicio 3: Pipeline selectivo con reporte
Implementa una función que procese un PDF con estrategia selectiva y genere un reporte: cuántas páginas con Vision, cuántas con OCR, y costo estimado.
Ver solución
def selective_with_report(pdf_path: str, prompt: str) -> dict:
images = pdf_to_images_pymupdf(pdf_path, dpi=150)
results = process_selective(images, prompt)
vision_pages = [r for r in results if r["method"] == "vision"]
ocr_pages = [r for r in results if r["method"] == "ocr"]
cost = len(vision_pages) * 0.003
return {
"total_pages": len(results),
"vision_pages": len(vision_pages),
"ocr_pages": len(ocr_pages),
"vision_nums": [r["page"] for r in vision_pages],
"estimated_cost": f"${cost:.4f}",
"savings": f"{(1 - len(vision_pages)/max(len(results),1))*100:.0f}%",
"pages": results
}
Ejercicio 4: Validar y comprimir antes de enviar
Escribe una función que reciba imágenes, verifique que el tamaño total no exceda el límite del proveedor, y si excede, comprima progresivamente hasta que quepa.
Ver solución
def validate_and_compress(images: list[Image.Image], max_total_mb: float = 20.0) -> list[dict]:
max_bytes = max_total_mb * 1024 * 1024
quality = 85
while quality >= 40:
pages_data = [prepare_page_for_vision(img, jpeg_quality=quality) for img in images]
total = sum(p["size_bytes"] for p in pages_data)
if total <= max_bytes:
return pages_data
quality -= 10
return [prepare_page_for_vision(img, max_side=1024, jpeg_quality=40) for img in images]
Recursos adicionales
- OpenAI Vision — Image limits and pricing
- Anthropic Vision — Image format requirements
- PyMuPDF (fitz) — Pixmap documentation
- pdf2image — GitHub repository
- PIL Image.resize