Módulo 2: Vision + LLMs

4. Gemini Vision (Google)

Descripción

Google Gemini representa un enfoque diferente al análisis visual con LLMs. Mientras que GPT-4 Vision (cápsula 02) y Claude 3 Vision (cápsula 03) ofrecen capacidades sólidas de visión, Gemini se distingue por tres ventajas concretas: un context window de 1 millón de tokens (vs ~128K de los otros), soporte nativo de video como input, y Gemini Flash, un modelo optimizado que reduce costos hasta ~33x comparado con GPT-4o.

La API de Google AI es estructuralmente diferente. En lugar del patrón messages → content de OpenAI/Anthropic, Gemini usa GenerativeModel + generate_content() con una lista plana de partes. Las imágenes se envían como bytes crudos, no como strings Base64.

Estas diferencias no son solo cosméticas — definen cuándo y por qué elegir Gemini sobre las alternativas.


Modelos con Vision

Google ofrece tres modelos principales con capacidad de visión:

ModeloContext WindowCalidad VisualCosto Input (1M tokens)VelocidadMejor uso
gemini-2.0-flash1M tokensAlta~$0.10Muy rápidaDefault general, producción
gemini-1.5-flash1M tokensBuena~$0.075Muy rápidaAlto volumen, costo mínimo
gemini-1.5-pro1M tokensMuy alta~$1.25ModeradaAnálisis complejo, documentos largos

Todos comparten el context window de 1M tokens. La diferencia está en calidad de razonamiento y costo.

¿Cuál elegir?

  • gemini-2.0-flash: Tu default. Balance entre calidad, velocidad y costo. Soporta las features más recientes.
  • gemini-1.5-flash: Cuando procesas miles de imágenes y cada centavo cuenta. Calidad ligeramente menor pero extremadamente barato.
  • gemini-1.5-pro: Cuando necesitas el mejor razonamiento posible: documentos complejos, análisis multi-página, tareas que requieren comprensión profunda.

Para comparar: GPT-4o cuesta ~$2.50/1M tokens de input. Gemini 1.5 Flash cuesta ~$0.075. Eso es ~33x más barato.

Costo estimado por 1,000 imágenes (~500 tokens/imagen):
  GPT-4o:           ~$1.25
  Gemini 1.5 Flash: ~$0.04  ← ~33x más barato

Setup y API

Instalación

pip install google-generativeai

Configuración

import os
import google.generativeai as genai

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

La API key se obtiene en Google AI Studio. A diferencia de OpenAI y Anthropic, Google ofrece un tier gratuito con límites generosos para experimentación.

Para verificar que funciona:

model = genai.GenerativeModel("gemini-2.0-flash")
response = model.generate_content("Hola, ¿funciona?")
print(response.text)

Estructura del Request — Diferencias

La diferencia fundamental entre Gemini y OpenAI/Anthropic está en cómo construyes el request.

Patrón Gemini

import google.generativeai as genai
from pathlib import Path

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

model = genai.GenerativeModel("gemini-2.0-flash")

image_part = {
    "mime_type": "image/jpeg",
    "data": Path("foto.jpg").read_bytes()
}

response = model.generate_content(["Describe esta imagen.", image_part])
print(response.text)

Observa las diferencias clave:

  1. No hay messages: En lugar de un array de mensajes con roles, Gemini recibe una lista plana de "partes" (texto, imágenes, etc.)
  2. Bytes crudos, no Base64: El campo data espera bytes directamente. No necesitas base64.b64encode().
  3. mime_type explícito: Similar a Anthropic, pero sin el wrapper source.
  4. Modelo como objeto: Creas un GenerativeModel una vez y llamas generate_content() múltiples veces.

Comparación lado a lado

AspectoOpenAIAnthropicGemini
ClienteOpenAI()Anthropic()genai.configure() + GenerativeModel()
Estructuramessages[].content[]messages[].content[]Lista plana de partes
Formato imagenData URI Base64source.base64 + media_typeDict {mime_type, data: bytes}
Encoding imagenBase64 stringBase64 stringRaw bytes
Responsechoices[0].message.contentcontent[0].textresponse.text
Rolessystem, user, assistantsystem (param), user, assistantImplícito en partes
Max tokensmax_tokens parammax_tokens (requerido)generation_config
Context window128K (GPT-4o)200K (Claude 3.5)1M tokens

¿Por qué bytes en lugar de Base64?

Base64 incrementa el tamaño ~33%. Para una imagen de 3MB, el string Base64 pesa ~4MB. Gemini evita esa conversión recibiendo bytes directamente, lo que reduce el payload del request. Esto importa especialmente cuando envías muchas imágenes o documentos grandes.


Input: Bytes vs URL vs File Upload

Gemini ofrece tres formas de enviar imágenes, cada una optimizada para un escenario diferente.

Método 1: Bytes directos (archivos locales)

from pathlib import Path

image_part = {
    "mime_type": "image/jpeg",
    "data": Path("foto.jpg").read_bytes()
}
response = model.generate_content(["Describe esta imagen.", image_part])

Cuándo usarlo: Archivos locales menores a ~20MB.

Método 2: URL pública

image_part = genai.types.Part.from_uri(
    "https://example.com/imagen.png",
    mime_type="image/png"
)
response = model.generate_content(["Describe esta imagen.", image_part])

Cuándo usarlo: Imágenes ya hospedadas en la web. Evita descargar + enviar.

Método 3: File API (archivos grandes)

Para archivos grandes (>20MB) o videos, Gemini ofrece un File API que sube el archivo primero y luego lo referencia:

uploaded = genai.upload_file("documento_largo.pdf", mime_type="application/pdf")

response = model.generate_content([
    "Resume los puntos clave de este documento.",
    uploaded
])
print(response.text)

Cuándo usarlo: PDFs grandes, videos, o cuando vas a reusar el mismo archivo en múltiples requests.

MétodoTamaño máxLatenciaReutilizableCaso de uso
Bytes directos~20MBBajaNoArchivos locales pequeños
URLDepende del hostMediaNoImágenes públicas
File API2GBAlta (upload)Videos, PDFs, archivos grandes

Múltiples Imágenes

Aquí es donde el context window de 1M tokens brilla. Enviar múltiples imágenes con Gemini es trivial — simplemente agregas más partes a la lista:

from pathlib import Path

model = genai.GenerativeModel("gemini-2.0-flash")

parts = ["Compara estas tres imágenes. ¿Qué tienen en común y en qué difieren?"]

image_paths = ["foto1.jpg", "foto2.jpg", "foto3.jpg"]
for path in image_paths:
    parts.append({
        "mime_type": "image/jpeg",
        "data": Path(path).read_bytes()
    })

response = model.generate_content(parts)
print(response.text)

Con 1M tokens de contexto, puedes enviar decenas de imágenes en un solo request sin preocuparte por el límite. Para OpenAI (128K) o Anthropic (200K), enviar más de 5-10 imágenes de alta resolución ya empieza a ser riesgoso.

Ejemplo práctico — analizar un catálogo completo:

from pathlib import Path

def analyze_catalog(image_dir: str, prompt: str) -> str:
    model = genai.GenerativeModel("gemini-1.5-flash")
    
    parts = [prompt]
    for img in sorted(Path(image_dir).glob("*.jpg")):
        parts.append({
            "mime_type": "image/jpeg",
            "data": img.read_bytes()
        })
    
    response = model.generate_content(parts)
    return response.text

result = analyze_catalog(
    "catalogo_productos/",
    "Clasifica cada producto por categoría y estima su rango de precio."
)

Ejemplo 1: Descripción de imagen

import os
import google.generativeai as genai
from pathlib import Path

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

def describe_image(image_path: str, detail_level: str = "medio") -> str:
    model = genai.GenerativeModel("gemini-2.0-flash")
    
    prompts = {
        "breve": "Describe esta imagen en una oración.",
        "medio": "Describe esta imagen en 2-3 oraciones, cubriendo los elementos principales.",
        "detallado": "Describe esta imagen en detalle: objetos, colores, composición, contexto y ambiente."
    }
    
    image_part = {
        "mime_type": "image/jpeg",
        "data": Path(image_path).read_bytes()
    }
    
    response = model.generate_content([prompts[detail_level], image_part])
    return response.text

print(describe_image("paisaje.jpg", "detallado"))

Ejemplo 2: OCR con Gemini

import os
import google.generativeai as genai
from pathlib import Path

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

def extract_text(image_path: str, preserve_layout: bool = False) -> str:
    model = genai.GenerativeModel("gemini-2.0-flash")
    
    if preserve_layout:
        prompt = (
            "Extrae TODO el texto visible en esta imagen. "
            "Mantén la estructura y formato original lo más fielmente posible. "
            "Si hay tablas, represéntalas con formato markdown."
        )
    else:
        prompt = (
            "Extrae todo el texto visible en esta imagen. "
            "Devuelve solo el texto, sin explicaciones adicionales."
        )
    
    image_part = {
        "mime_type": "image/png",
        "data": Path(image_path).read_bytes()
    }
    
    response = model.generate_content([prompt, image_part])
    return response.text

text = extract_text("recibo.png", preserve_layout=True)
print(text)

Ejemplo 3: Análisis de documento largo

Esta es una de las fortalezas únicas de Gemini. Con 1M tokens de contexto, puedes enviar un PDF de 20+ páginas y analizarlo completo — algo que GPT-4o (128K) y Claude (200K) no pueden hacer con documentos tan extensos.

import os
import google.generativeai as genai

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

def analyze_long_document(pdf_path: str, question: str) -> str:
    uploaded_file = genai.upload_file(pdf_path, mime_type="application/pdf")
    
    model = genai.GenerativeModel("gemini-1.5-pro")
    
    response = model.generate_content([
        f"""Analiza este documento completo y responde la siguiente pregunta:

{question}

Basa tu respuesta únicamente en el contenido del documento. 
Si la información no está en el documento, indícalo explícitamente.""",
        uploaded_file
    ])
    return response.text

result = analyze_long_document(
    "reporte_anual_2024.pdf",
    "¿Cuáles fueron los tres principales riesgos identificados y qué mitigaciones se propusieron?"
)
print(result)

Comparación de capacidad para documentos:

Documento de 50 páginas (~75,000 tokens):
  GPT-4o (128K):     ✅ Cabe, pero queda poco espacio para respuesta
  Claude 3.5 (200K): ✅ Cabe con margen
  Gemini 1.5 Pro (1M): ✅ Usa solo ~7.5% del contexto

Documento de 200 páginas (~300,000 tokens):
  GPT-4o (128K):     ❌ No cabe
  Claude 3.5 (200K): ❌ No cabe (o muy justo)
  Gemini 1.5 Pro (1M): ✅ Usa solo ~30% del contexto

Ejemplo 4: Análisis de Video

Gemini es el único de los tres proveedores que soporta video como input nativo. Puedes enviar un video y hacer preguntas sobre su contenido visual y auditivo.

import os
import time
import google.generativeai as genai

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

def analyze_video(video_path: str, prompt: str) -> str:
    video_file = genai.upload_file(video_path)
    
    while video_file.state.name == "PROCESSING":
        time.sleep(5)
        video_file = genai.get_file(video_file.name)
    
    if video_file.state.name == "FAILED":
        raise ValueError(f"El procesamiento del video falló: {video_file.state.name}")
    
    model = genai.GenerativeModel("gemini-2.0-flash")
    response = model.generate_content([prompt, video_file])
    return response.text

result = analyze_video(
    "demo_producto.mp4",
    "Describe paso a paso lo que sucede en este video. "
    "Identifica las acciones principales y cualquier texto visible."
)
print(result)

El File API procesa el video de forma asíncrona. El loop de while espera hasta que esté listo. Formatos soportados: MP4, MOV, AVI, MKV, WEBM, entre otros.

Esto abre casos de uso que no existen en OpenAI o Anthropic: QA de videos de capacitación, análisis de demos de producto, monitoreo de grabaciones. Con los otros proveedores, la alternativa es extraer frames como imágenes individuales — Gemini lo maneja nativamente.


Safety Settings

Gemini incluye filtros de seguridad que pueden bloquear respuestas sin aviso claro. Esto es un pitfall común — tu código funciona pero response.text lanza una excepción.

El problema

response = model.generate_content(["Analiza esta imagen médica.", image_part])
print(response.text)  # ValueError: response has no candidates

Gemini puede considerar que la imagen o el prompt violan sus políticas y bloquear la respuesta completa.

La solución

Configura safety_settings para ajustar los umbrales:

from google.generativeai.types import HarmCategory, HarmBlockThreshold

safety_config = {
    HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
    HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
    HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
    HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
}

model = genai.GenerativeModel("gemini-2.0-flash", safety_settings=safety_config)
response = model.generate_content(["Analiza esta imagen médica.", image_part])

Siempre verifica response.candidates antes de acceder a response.text — si fue bloqueado, candidates estará vacío o tendrá finish_reason == "SAFETY".


Tokens y Costos

Leer el uso de tokens

response = model.generate_content(["Describe esta imagen.", image_part])

print(f"Tokens de input:  {response.usage_metadata.prompt_token_count}")
print(f"Tokens de output: {response.usage_metadata.candidates_token_count}")
print(f"Tokens totales:   {response.usage_metadata.total_token_count}")

Comparación de costos entre proveedores

ProveedorModeloInput (1M tokens)Output (1M tokens)Relativo
GoogleGemini 1.5 Flash$0.075$0.301x (base)
GoogleGemini 2.0 Flash$0.10$0.401.3x
GoogleGemini 1.5 Pro$1.25$5.0017x
OpenAIGPT-4o$2.50$10.0033x
OpenAIGPT-4o-mini$0.15$0.602x
AnthropicClaude 3.5 Sonnet$3.00$15.0040x

Para tareas de visión de alto volumen (e.g., procesar catálogos, OCR masivo, clasificación de imágenes), Gemini Flash puede reducir costos drásticamente.

Estimar costos antes de ejecutar

def estimate_cost_flash(image_paths: list[str], avg_output_tokens: int = 200) -> float:
    estimated_tokens_per_image = 500
    total_input = len(image_paths) * estimated_tokens_per_image
    total_output = len(image_paths) * avg_output_tokens
    
    cost_input = (total_input / 1_000_000) * 0.075
    cost_output = (total_output / 1_000_000) * 0.30
    
    return cost_input + cost_output

cost = estimate_cost_flash(["img1.jpg"] * 10_000)
print(f"Costo estimado para 10,000 imágenes: ${cost:.4f}")

Troubleshooting

1. response has no candidates / Respuesta bloqueada

Causa: Los safety filters bloquearon la respuesta.

response = model.generate_content([prompt, image_part])

if not response.candidates:
    print("Bloqueado. Razón:", response.prompt_feedback)
else:
    finish = response.candidates[0].finish_reason.name
    if finish == "SAFETY":
        print("Bloqueado por safety. Ratings:")
        for r in response.candidates[0].safety_ratings:
            print(f"  {r.category.name}: {r.probability.name}")
    else:
        print(response.text)

Solución: Configurar safety_settings como se mostró en la sección anterior.

2. google.api_core.exceptions.InvalidArgument: API key not valid

Causa: API key incorrecta o no configurada.

import os
key = os.environ.get("GOOGLE_API_KEY", "NO DEFINIDA")
print(f"Key presente: {'Sí' if key != 'NO DEFINIDA' else 'No'}")
print(f"Key empieza con: {key[:8]}..." if key != "NO DEFINIDA" else "")

Solución: Verificar en Google AI Studio que la key esté activa y que el proyecto tenga la API habilitada.

3. File upload failed / Timeout en archivos grandes

Causa: Archivo demasiado grande o conexión lenta.

uploaded = genai.upload_file("video_grande.mp4")

import time
timeout = 300
start = time.time()
while uploaded.state.name == "PROCESSING":
    if time.time() - start > timeout:
        raise TimeoutError(f"Upload processing exceeded {timeout}s")
    time.sleep(10)
    uploaded = genai.get_file(uploaded.name)

Solución: Para videos grandes, esperar pacientemente. El procesamiento puede tomar minutos.

4. models/gemini-xxx is not found

Causa: Nombre de modelo incorrecto o modelo deprecated.

for m in genai.list_models():
    if "vision" in m.name or "gemini" in m.name:
        print(f"{m.name} — soporta generateContent: {m.supported_generation_methods}")

Solución: Listar modelos disponibles y verificar el nombre exacto. Google renombra modelos frecuentemente.

5. Resource exhausted / Rate limiting

Causa: Demasiadas requests por minuto.

import time

def generate_with_retry(model, parts, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            return model.generate_content(parts)
        except Exception as e:
            if "Resource exhausted" in str(e) and attempt < max_retries - 1:
                wait = 2 ** attempt * 10
                print(f"Rate limited. Esperando {wait}s...")
                time.sleep(wait)
            else:
                raise

Ejercicios

Ejercicio 1 (Fácil): Función configurable de análisis

Escribe una función gemini_analyze que reciba un path de imagen, un prompt, y opcionalmente un modelo (default gemini-2.0-flash). Debe retornar el texto de la respuesta. Detecta automáticamente el mime_type basándote en la extensión del archivo.

Ver solución
import os
import google.generativeai as genai
from pathlib import Path

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

MIME_TYPES = {
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".gif": "image/gif",
    ".webp": "image/webp",
}

def gemini_analyze(
    image_path: str,
    prompt: str,
    model_name: str = "gemini-2.0-flash"
) -> str:
    path = Path(image_path)
    mime_type = MIME_TYPES.get(path.suffix.lower(), "image/jpeg")
    
    model = genai.GenerativeModel(model_name)
    image_part = {
        "mime_type": mime_type,
        "data": path.read_bytes()
    }
    
    response = model.generate_content([prompt, image_part])
    return response.text

result = gemini_analyze("foto.png", "¿Qué ves en esta imagen?")
print(result)

Ejercicio 2 (Medio): Procesador batch con Gemini Flash

Crea una función batch_analyze que reciba una lista de paths de imágenes y un prompt, procese todas con gemini-1.5-flash (el más barato), y retorne un diccionario {filename: response}. Incluye: detección de mime type, manejo de errores por imagen individual (si una falla, continuar con las demás), y un reporte final con tokens totales usados.

Ver solución
import os
import google.generativeai as genai
from pathlib import Path

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

MIME_TYPES = {
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".gif": "image/gif",
    ".webp": "image/webp",
}

def batch_analyze(
    image_paths: list[str],
    prompt: str
) -> dict:
    model = genai.GenerativeModel("gemini-1.5-flash")
    results = {}
    total_input_tokens = 0
    total_output_tokens = 0
    errors = 0
    
    for img_path in image_paths:
        path = Path(img_path)
        filename = path.name
        
        try:
            mime_type = MIME_TYPES.get(path.suffix.lower(), "image/jpeg")
            image_part = {
                "mime_type": mime_type,
                "data": path.read_bytes()
            }
            
            response = model.generate_content([prompt, image_part])
            results[filename] = response.text
            
            total_input_tokens += response.usage_metadata.prompt_token_count
            total_output_tokens += response.usage_metadata.candidates_token_count
            
        except Exception as e:
            results[filename] = f"ERROR: {e}"
            errors += 1
    
    cost_input = (total_input_tokens / 1_000_000) * 0.075
    cost_output = (total_output_tokens / 1_000_000) * 0.30
    
    print(f"\n--- Reporte Batch ---")
    print(f"Imágenes procesadas: {len(image_paths) - errors}/{len(image_paths)}")
    print(f"Errores: {errors}")
    print(f"Tokens input: {total_input_tokens:,}")
    print(f"Tokens output: {total_output_tokens:,}")
    print(f"Costo estimado: ${cost_input + cost_output:.6f}")
    
    return results

images = ["img1.jpg", "img2.png", "img3.jpg"]
results = batch_analyze(images, "Describe esta imagen en una oración.")

for name, text in results.items():
    print(f"\n{name}: {text}")

Ejercicio 3 (Medio): Comparador multi-proveedor

Crea una función compare_providers que tome un path de imagen y un prompt, envíe la misma imagen a OpenAI (gpt-4o-mini), Anthropic (claude-sonnet-4-20250514) y Google (gemini-2.0-flash), y retorne un diccionario con la respuesta y latencia de cada uno. Usa time.time() para medir la latencia.

Ver solución
import os
import time
import base64
import google.generativeai as genai
from pathlib import Path
from openai import OpenAI
import anthropic

genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

def compare_providers(image_path: str, prompt: str) -> dict:
    path = Path(image_path)
    raw_bytes = path.read_bytes()
    b64_string = base64.b64encode(raw_bytes).decode()
    results = {}
    
    # --- OpenAI ---
    start = time.time()
    try:
        client_oai = OpenAI()
        resp_oai = client_oai.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {"type": "image_url", "image_url": {
                        "url": f"data:image/jpeg;base64,{b64_string}"
                    }}
                ]
            }],
            max_tokens=500
        )
        results["openai"] = {
            "response": resp_oai.choices[0].message.content,
            "latency_s": round(time.time() - start, 2)
        }
    except Exception as e:
        results["openai"] = {"response": f"ERROR: {e}", "latency_s": round(time.time() - start, 2)}
    
    # --- Anthropic ---
    start = time.time()
    try:
        client_ant = anthropic.Anthropic()
        resp_ant = client_ant.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=500,
            messages=[{
                "role": "user",
                "content": [
                    {"type": "image", "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": b64_string
                    }},
                    {"type": "text", "text": prompt}
                ]
            }]
        )
        results["anthropic"] = {
            "response": resp_ant.content[0].text,
            "latency_s": round(time.time() - start, 2)
        }
    except Exception as e:
        results["anthropic"] = {"response": f"ERROR: {e}", "latency_s": round(time.time() - start, 2)}
    
    # --- Gemini ---
    start = time.time()
    try:
        model_gem = genai.GenerativeModel("gemini-2.0-flash")
        resp_gem = model_gem.generate_content([
            prompt,
            {"mime_type": "image/jpeg", "data": raw_bytes}
        ])
        results["gemini"] = {
            "response": resp_gem.text,
            "latency_s": round(time.time() - start, 2)
        }
    except Exception as e:
        results["gemini"] = {"response": f"ERROR: {e}", "latency_s": round(time.time() - start, 2)}
    
    print("\n=== Comparación Multi-Proveedor ===\n")
    for provider, data in results.items():
        print(f"--- {provider.upper()} ({data['latency_s']}s) ---")
        print(data["response"][:300])
        print()
    
    return results

compare_providers("test.jpg", "Describe esta imagen en 2 oraciones.")

Resumen

  • API: genai.configure() + GenerativeModel() + generate_content([partes])
  • Imagen como bytes: Dict {"mime_type": ..., "data": bytes} — sin Base64
  • Tres métodos de input: bytes directos, URL con Part.from_uri(), File API para archivos grandes
  • Context window de 1M tokens: Permite documentos largos y múltiples imágenes donde otros modelos no pueden
  • Video nativo: Único proveedor con soporte directo de video como input
  • Gemini Flash: ~33x más barato que GPT-4o para tareas de visión de alto volumen
  • Safety settings: Configurar explícitamente para evitar bloqueos inesperados

Recursos adicionales

  1. Google AI Python SDK
  2. Gemini Vision Guide
  3. Gemini File API
  4. Modelos y Precios
  5. Safety Settings