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:
| Modelo | Context Window | Calidad Visual | Costo Input (1M tokens) | Velocidad | Mejor uso |
|---|---|---|---|---|---|
gemini-2.0-flash | 1M tokens | Alta | ~$0.10 | Muy rápida | Default general, producción |
gemini-1.5-flash | 1M tokens | Buena | ~$0.075 | Muy rápida | Alto volumen, costo mínimo |
gemini-1.5-pro | 1M tokens | Muy alta | ~$1.25 | Moderada | Aná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:
- No hay
messages: En lugar de un array de mensajes con roles, Gemini recibe una lista plana de "partes" (texto, imágenes, etc.) - Bytes crudos, no Base64: El campo
dataesperabytesdirectamente. No necesitasbase64.b64encode(). mime_typeexplícito: Similar a Anthropic, pero sin el wrappersource.- Modelo como objeto: Creas un
GenerativeModeluna vez y llamasgenerate_content()múltiples veces.
Comparación lado a lado
| Aspecto | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| Cliente | OpenAI() | Anthropic() | genai.configure() + GenerativeModel() |
| Estructura | messages[].content[] | messages[].content[] | Lista plana de partes |
| Formato imagen | Data URI Base64 | source.base64 + media_type | Dict {mime_type, data: bytes} |
| Encoding imagen | Base64 string | Base64 string | Raw bytes |
| Response | choices[0].message.content | content[0].text | response.text |
| Roles | system, user, assistant | system (param), user, assistant | Implícito en partes |
| Max tokens | max_tokens param | max_tokens (requerido) | generation_config |
| Context window | 128K (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étodo | Tamaño máx | Latencia | Reutilizable | Caso de uso |
|---|---|---|---|---|
| Bytes directos | ~20MB | Baja | No | Archivos locales pequeños |
| URL | Depende del host | Media | No | Imágenes públicas |
| File API | 2GB | Alta (upload) | Sí | 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
| Proveedor | Modelo | Input (1M tokens) | Output (1M tokens) | Relativo |
|---|---|---|---|---|
| Gemini 1.5 Flash | $0.075 | $0.30 | 1x (base) | |
| Gemini 2.0 Flash | $0.10 | $0.40 | 1.3x | |
| Gemini 1.5 Pro | $1.25 | $5.00 | 17x | |
| OpenAI | GPT-4o | $2.50 | $10.00 | 33x |
| OpenAI | GPT-4o-mini | $0.15 | $0.60 | 2x |
| Anthropic | Claude 3.5 Sonnet | $3.00 | $15.00 | 40x |
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