cd ../back to blog
$Guide//June 4, 2026//7 min read

Visión y comprensión de documentos: OCR, gráficos y escaneos con Claude y Gemini

Adjunta imágenes como una parte image_url de OpenAI (URL o base64) y léelas con Claude y Gemini a través de Brievio: OCR, gráficos, recibos y escaneos, con la misma petición.

Una cantidad sorprendente de lo que se vende como «IA documental» no es más que enviar una imagen a un modelo de chat y leer lo que responde. Un recibo, la captura de un panel, la página de un PDF escaneado, la foto de una pizarra — los modelos modernos de Claude y Gemini lo leen todo de forma nativa, sin necesidad de un motor de OCR aparte. El problema suele ser de fontanería: cada proveedor tiene su propia manera de adjuntar una imagen, y portar el código de uno a otro es un fastidio.

A través de Brievio usas una sola forma de petición — el array content de Chat Completions de OpenAI con una parte image_url — y funciona de forma idéntica contra Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 y Gemini 2.5 Pro / Flash. Son los modelos genuinos de primera mano con su visión nativa respetada, así que el mismo JPEG que Claude lee bien, lo lee Claude de verdad. Esta entrada trata sobre la entrada de imágenes (comprensión), no sobre la generación de imágenes: URLs, base64, prompts con varias imágenes y los patrones de OCR / gráficos / documentos escaneados que aparecen en el trabajo real.

El caso más simple: una imagen por URL

Si tu imagen ya vive en una URL pública HTTPS, adjúntala como una parte image_url junto a tu texto. Cambia claude-sonnet-4-6 por gemini-2.5-pro y el cuerpo de la petición no cambia — esa portabilidad es justo el objetivo:

image_url.py
# Envía una imagen por URL. La misma forma de chat de OpenAI, contra el modelo genuino.
from openai import OpenAI

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
)

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",     # o gemini-2.5-pro — la misma forma de petición
    max_tokens=500,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "¿Qué muestra este gráfico? Da la tendencia en una frase."},
                {
                    "type": "image_url",
                    "image_url": {"url": "https://example.com/q3-revenue.png"},
                },
            ],
        }
    ],
)

print(resp.choices[0].message.content)
# Las imágenes se facturan como tokens de INPUT — lee resp.usage.prompt_tokens para ver el coste.

Algo que conviene interiorizar pronto: las imágenes cuestan tokens de input. Un modelo no ve los píxeles gratis — divide la imagen en mosaicos y cada mosaico se factura como texto. Brievio reporta conteos de tokens honestos, así que el coste de la imagen aparece en resp.usage.prompt_tokens exactamente como lo cobra el proveedor original. Una captura a pantalla completa puede ir de unos cientos a un par de miles de tokens de input según la resolución. Presupuéstalo como presupuestarías un párrafo de contexto, no como algo gratis.

Base64: el caso que realmente vas a usar

En producción la imagen rara vez es una URL pública — es un archivo que un usuario acaba de subir, un búfer de un escáner, un objeto privado de S3. Para esos casos, incluye los bytes en línea como una data URL data: en base64. El modelo no nota la diferencia; tus bytes nunca tienen que ser accesibles públicamente:

base64_upload.py
# La mayoría de las imágenes en producción no son URLs públicas. Inclúyelas en línea como data URLs en base64.
import base64
from openai import OpenAI

client = OpenAI(api_key="sk-brievio-...", base_url="https://api.brievio.com/v1")

def data_url(path: str, media_type: str = "image/png") -> str:
    with open(path, "rb") as f:
        b64 = base64.standard_b64encode(f.read()).decode("utf-8")
    return f"data:{media_type};base64,{b64}"

resp = client.chat.completions.create(
    model="gemini-2.5-flash",      # barato + rápido para OCR / recibos
    max_tokens=800,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Extrae cada línea de detalle y el total como JSON. Claves: items[], total."},
                {"type": "image_url", "image_url": {"url": data_url("receipt.jpg", "image/jpeg")}},
            ],
        }
    ],
)

print(resp.choices[0].message.content)
# Consejo: las data URLs inflan el cuerpo de la petición ~33% sobre el archivo original. Mantén las imágenes
# a un tamaño razonable — una captura de 2-3MP basta de sobra para texto; rara vez necesitas 12MP.

Dos salvedades prácticas. Primera, base64 añade alrededor de un 33% de sobrecarga al cuerpo de tu petición, y hay límites de tamaño por imagen (Anthropic limita las imágenes individuales en torno a 5 MB en la API; Gemini tiene su propio techo). Si un escaneo grande da un 413, redúcelo de escala — el texto sigue siendo legible a una resolución mucho más baja de lo que imaginas. Segunda, envía el media_type correcto (image/png, image/jpeg, image/webp); un tipo que no coincide es una causa común de un fallo silencioso de decodificación. Cuando una petición sí falla con un 4xx o 5xx en Brievio, no se te cobra por ella — las llamadas fallidas son gratis, así que puedes reintentar con una imagen reducida sin pagar dos veces.

Prompts con varias imágenes y documentos escaneados

El array content admite tantas partes image_url como quieras, intercaladas con texto. Eso desbloquea los flujos de trabajo de verdad útiles: comparar una captura de antes/después, leer un documento escaneado de varias páginas o pasarle una secuencia de gráficos y pedir el hilo común. El truco que da resultado en ambas familias de modelos es etiquetar cada imagen con una pequeña ancla de texto para que el modelo pueda citarla:

multi_image.py
# Varias imágenes en un solo prompt — compara dos capturas, o lee un escaneo de 4 páginas.
content = [
    {"type": "text", "text": "Estas son las páginas 1-3 de un contrato escaneado. Resume las partes, el plazo y la cláusula de rescisión. Cita el número de página en cada caso."},
]
for i, path in enumerate(["page1.png", "page2.png", "page3.png"], start=1):
    content.append({"type": "text", "text": f"--- Página {i} ---"})
    content.append({"type": "image_url", "image_url": {"url": data_url(path)}})

resp = client.chat.completions.create(
    model="claude-opus-4-7",       # el razonamiento más fuerte sobre documentos densos
    max_tokens=1200,
    messages=[{"role": "user", "content": content}],
)

print(resp.choices[0].message.content)
# Intercalar una etiqueta de texto antes de cada imagen ("--- Página 2 ---") le da al modelo
# un ancla para citar, y mejora notablemente el anclaje multi-imagen en ambas familias.

Para documentos largos hay un compromiso en la elección del modelo. Gemini 2.5 Flash es barato y rápido, y es una opción por defecto estupenda para OCR de alto volumen, recibos y extracción de formularios. Claude Opus 4.7 razona con más fuerza sobre material denso y de varias páginas — contratos, estados financieros, cualquier cosa donde necesites que mantenga varias páginas a la vista y las cruce entre sí. Sonnet 4.6 y Gemini 2.5 Pro quedan en un punto intermedio. Puedes enrutar según la tarea sin cambiar nada del código salvo la cadena model; mira la lista en vivo en /models.

En qué son buenos (y en qué no) estos modelos

La visión nativa resuelve bien un amplio abanico de tareas reales:

  • OCR y transcripción — texto impreso, y una caligrafía sorprendentemente decente. Sin un pipeline de Tesseract que mantener.
  • Gráficos y paneles — leer valores de gráficos de barras/líneas, resumir una tendencia, comprobar la coherencia de una métrica en una captura.
  • Extracción estructurada — recibos, facturas, formularios e identificaciones a JSON. Combínalo con un esquema estricto en tu prompt para una salida limpia.
  • Comprensión de interfaces y diagramas — describir una pantalla, leer un cuadro de diálogo de error, explicar un diagrama de arquitectura.

Y algunos límites honestos. Los modelos todavía de vez en cuando leen mal un dígito suelto o la celda de una tabla densa, así que para cualquier cosa donde un número equivocado salga caro, valida contra un esquema o una suma de control (por ejemplo, las líneas de detalle deberían sumar el total indicado). El texto diminuto en una imagen de baja resolución es el fallo más común — dale un recorte de mayor resolución. Y el comportamiento por modelo difiere de verdad: una maquetación que un modelo clava, otro puede tropezar con ella, que es justo por lo que poder cambiar de modelo detrás de una sola API y compararlos en A/B sobre tus propios documentos vale más que cualquier benchmark aislado.

Yendo más allá: visión más herramientas

La visión se combina con el resto de la API. Puedes pasarle al modelo una imagen y un conjunto de herramientas, de modo que lea una captura y luego llame a una función con lo que extrajo — «lee esta factura, luego llama a create_expense(amount, vendor, date)». Esa capa de llamada a herramientas también es uniforme entre Claude y Gemini a través de Brievio; la guía de uso de herramientas cubre la forma compartida. Combinada con la visión, es casi todo un pipeline de procesamiento de documentos en una sola petición.

La conclusión

Para la comprensión de imágenes no necesitas un servicio de OCR aparte ni SDKs específicos de cada proveedor. Adjunta una parte image_url — URL o data URL en base64 — a tu petición de chat normal, etiqueta varias imágenes para que el modelo pueda citarlas, y enruta al modelo que encaje con la tarea: Gemini Flash para lecturas baratas de alto volumen, Claude Opus para documentos difíciles. Recuerda que las imágenes se facturan como tokens de input (mostrados con honestidad en usage), mantén la resolución sensata, valida los números extraídos y apóyate en los reintentos gratis-si-falla cuando un escaneo grande rebote. La referencia completa de la petición y los límites por modelo están en la documentación — empieza ahí y tendrás documentos fluyendo por Claude o Gemini en una tarde.