Módulo 1: Introducción a IA Multimodal

4. Combinaciones Multimodales

Descripción

Hasta ahora viste visión (imagen → texto) y audio (audio ↔ texto) como modalidades independientes. Pero el verdadero poder de la IA multimodal aparece cuando las combinas en pipelines: imagen + texto → análisis, audio → texto → LLM → imagen, documento → extracción → resumen en audio. En esta cápsula aprenderás los patrones de combinación más usados y cómo diseñar pipelines multimodales efectivos.

Por qué importa: En producción, rara vez usas una sola modalidad. Un sistema de análisis documental combina visión (leer el documento), texto (procesarlo con LLM) y audio (generar resumen hablado). Un asistente de soporte combina audio (escuchar pregunta), texto (procesarla) y visión (mostrar capturas). Dominar combinaciones es lo que diferencia un "script con API" de un "sistema multimodal".

Conexión con el módulo: Esta cápsula conecta las cápsulas 02 (visión) y 03 (audio) en una visión integrada. Los pipelines que verás aquí son la base del Clasificador multimodal (cápsula 08) y del Document Analyzer final (módulo 8).


Tipos de Combinaciones

Combinación por input

Envías múltiples modalidades como input al mismo modelo o pipeline.

Texto + Imagen → LLM con visión → Texto
"¿Qué muestra este gráfico?" + [grafico.png] → "El gráfico muestra ventas Q4..."

Combinación por pipeline

Encadenas modelos que procesan diferentes modalidades en secuencia.

Audio → Whisper → Texto → GPT-4o → Texto → TTS → Audio
[reunion.mp3] → "Resumen: ventas subieron 15%" → [resumen.mp3]

Combinación por output

Un sistema genera outputs en múltiples modalidades.

Documento PDF → Vision + LLM →
  ├── Texto: datos extraídos (JSON)
  ├── Imagen: gráfico generado (DALL-E)
  └── Audio: resumen hablado (TTS)

Los 6 Patrones de Pipelines Multimodales

Patrón 1: Vision + LLM (el más básico)

Envías imagen + prompt a un modelo con visión. Es el patrón más simple y más usado.

from openai import OpenAI
import base64

client = OpenAI()

def vision_plus_llm(image_path: str, question: str) -> str:
    """Patrón básico: imagen + pregunta → respuesta."""
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode()

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": question},
                {"type": "image_url", "image_url": {
                    "url": f"data:image/jpeg;base64,{image_data}"
                }}
            ]
        }],
        max_tokens=500
    )
    return response.choices[0].message.content

# Uso
answer = vision_plus_llm(
    "diagrama_arquitectura.png",
    "¿Cuántos microservicios tiene este sistema y cuáles son?"
)
print(answer)

Casos de uso: Q&A sobre imágenes, análisis de documentos, descripción de productos.

Patrón 2: Audio → Texto → LLM

Transcribes audio y luego lo procesas con un LLM. Viste esto en la cápsula 03, aquí lo formalizamos.

def audio_to_analysis(audio_path: str, analysis_prompt: str) -> str:
    """Patrón: audio → transcripción → análisis con LLM."""
    # Paso 1: Transcribir
    with open(audio_path, "rb") as f:
        transcript = client.audio.transcriptions.create(
            model="whisper-1",
            file=f,
            language="es"
        ).text

    # Paso 2: Analizar
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": analysis_prompt},
            {"role": "user", "content": f"Transcripción:\n\n{transcript}"}
        ],
        max_tokens=500
    )
    return response.choices[0].message.content

# Uso: extraer action items de una reunión
result = audio_to_analysis(
    "standup.mp3",
    "Extrae los action items de esta reunión. Lista cada uno con responsable y deadline."
)

Casos de uso: Resumen de reuniones, análisis de llamadas de soporte, extracción de datos de entrevistas.

Patrón 3: Texto → LLM → Imagen

Generas texto con un LLM y luego creas una imagen basada en ese texto. Útil para automatizar contenido visual.

def text_to_visual(topic: str) -> dict:
    """Patrón: tema → descripción → imagen generada."""
    # Paso 1: LLM genera prompt para imagen
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": (
                f"Genera un prompt en inglés para DALL-E que cree una "
                f"ilustración profesional sobre: {topic}\n\n"
                f"El prompt debe ser descriptivo (estilo, colores, composición). "
                f"Máximo 100 palabras."
            )
        }],
        max_tokens=150
    )
    image_prompt = response.choices[0].message.content

    # Paso 2: Generar imagen
    image_response = client.images.generate(
        model="dall-e-3",
        prompt=image_prompt,
        size="1024x1024",
        quality="standard",
        n=1
    )
    image_url = image_response.data[0].url

    return {
        "topic": topic,
        "image_prompt": image_prompt,
        "image_url": image_url
    }

# Uso
result = text_to_visual("cloud computing y microservicios")
print(f"Prompt generado: {result['image_prompt']}")
print(f"Imagen: {result['image_url']}")

Casos de uso: Generación automática de thumbnails, ilustraciones para artículos, assets de marketing.

Patrón 4: Documento → Extracción → JSON

Combinas visión (para leer el documento) con LLM (para estructurar datos). Es el pipeline core del módulo 3.

import json

def document_to_structured(image_path: str, fields: dict) -> dict:
    """Patrón: imagen de documento → datos estructurados."""
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode()

    fields_str = "\n".join(f"- {k}: {v}" for k, v in fields.items())

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": (
                        f"Extrae estos campos del documento:\n{fields_str}\n\n"
                        f"Responde SOLO con JSON válido. "
                        f"Usa null si un campo no es visible."
                    )
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/jpeg;base64,{image_data}",
                        "detail": "high"
                    }
                }
            ]
        }],
        max_tokens=500,
        temperature=0
    )
    return json.loads(response.choices[0].message.content)

# Uso: extraer datos de factura
data = document_to_structured("factura.jpg", {
    "numero_factura": "número de factura",
    "fecha": "fecha en formato YYYY-MM-DD",
    "total": "monto total con decimales",
    "empresa": "nombre del emisor"
})

Casos de uso: Procesamiento de facturas, formularios, recibos, contratos.

Patrón 5: Pipeline completo (audio + visión + texto)

El pipeline más complejo: combina todas las modalidades disponibles.

def full_multimodal_pipeline(
    audio_path: str = None,
    image_path: str = None,
    text_input: str = None
) -> dict:
    """Pipeline que acepta cualquier combinación de inputs."""
    context_parts = []

    # Procesar audio si existe
    if audio_path:
        with open(audio_path, "rb") as f:
            transcript = client.audio.transcriptions.create(
                model="whisper-1", file=f
            ).text
        context_parts.append(f"[Audio transcrito]: {transcript}")

    # Procesar imagen si existe
    if image_path:
        with open(image_path, "rb") as f:
            img_b64 = base64.b64encode(f.read()).decode()

        img_desc = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{
                "role": "user",
                "content": [
                    {"type": "text", "text": "Describe esta imagen en detalle."},
                    {"type": "image_url", "image_url": {
                        "url": f"data:image/jpeg;base64,{img_b64}"
                    }}
                ]
            }],
            max_tokens=300
        ).choices[0].message.content
        context_parts.append(f"[Imagen analizada]: {img_desc}")

    # Agregar texto si existe
    if text_input:
        context_parts.append(f"[Texto del usuario]: {text_input}")

    # Combinar y procesar
    combined = "\n\n".join(context_parts)
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    "Analiza toda la información proporcionada "
                    "(puede incluir audio transcrito, descripción de imagen, "
                    "y texto). Da un análisis integrado."
                )
            },
            {"role": "user", "content": combined}
        ],
        max_tokens=500
    )

    return {
        "inputs": {
            "audio": audio_path is not None,
            "image": image_path is not None,
            "text": text_input is not None
        },
        "context": combined,
        "analysis": response.choices[0].message.content
    }

Casos de uso: Asistentes multimodales, sistemas de soporte, análisis de contenido mixto.

Patrón 6: Generación → Análisis (ciclo cerrado)

Generas contenido y luego lo analizas para validar calidad.

def generate_and_validate(prompt: str) -> dict:
    """Genera imagen y la valida automáticamente."""
    # Paso 1: Generar imagen
    image_response = client.images.generate(
        model="dall-e-3",
        prompt=prompt,
        size="1024x1024",
        n=1
    )
    image_url = image_response.data[0].url

    # Paso 2: Analizar la imagen generada
    validation = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": (
                        f"Se pidió generar: '{prompt}'\n\n"
                        f"¿La imagen cumple con lo solicitado?\n"
                        f"Responde con JSON: "
                        f'{{"cumple": true/false, "razon": "...", '
                        f'"calidad": 1-10}}'
                    )
                },
                {"type": "image_url", "image_url": {"url": image_url}}
            ]
        }],
        max_tokens=200,
        temperature=0
    ).choices[0].message.content

    return {
        "prompt": prompt,
        "image_url": image_url,
        "validation": json.loads(validation)
    }

Casos de uso: QA automático de contenido generado, pipelines de generación con validación.


Diseñando Pipelines: Principios

1. Cada paso tiene un propósito claro

❌ MALO: Audio → Texto → LLM → LLM → LLM → Texto
   (¿por qué 3 pasos de LLM?)

✅ BUENO: Audio → Whisper(transcribir) → GPT-4o-mini(resumir) → TTS(audio)
   (cada paso hace algo distinto)

2. Usa el modelo más barato que funcione

# Para tareas simples: gpt-4o-mini ($0.15/1M tokens)
# Para vision compleja: gpt-4o ($2.50/1M tokens)
# Para transcripción: whisper-1 ($0.006/min)
# Para TTS estándar: tts-1 ($15/1M chars)

3. Maneja errores en cada paso

def safe_pipeline(audio_path: str) -> dict:
    result = {"status": "ok", "errors": []}

    # Paso 1: Transcribir
    try:
        transcript = transcribe_audio(audio_path)
    except Exception as e:
        result["errors"].append(f"Transcripción falló: {e}")
        result["status"] = "partial"
        transcript = None

    # Paso 2: Resumir (solo si paso 1 tuvo éxito)
    summary = None
    if transcript:
        try:
            summary = summarize_text(transcript)
        except Exception as e:
            result["errors"].append(f"Resumen falló: {e}")
            result["status"] = "partial"

    result["transcript"] = transcript
    result["summary"] = summary
    return result

4. Mide latencia y costo

import time

def timed_pipeline(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    elapsed = time.time() - start
    return {
        "result": result,
        "latency_seconds": round(elapsed, 2)
    }

Troubleshooting

Problema 1: Pipeline lento (>30 segundos)

Causa: Múltiples llamadas API secuenciales.

Solución: Paralelizar pasos independientes con asyncio o concurrent.futures.

from concurrent.futures import ThreadPoolExecutor

def parallel_analysis(image_path: str, audio_path: str):
    with ThreadPoolExecutor(max_workers=2) as executor:
        image_future = executor.submit(describe_image, image_path)
        audio_future = executor.submit(transcribe_audio, audio_path)

        image_desc = image_future.result()
        transcript = audio_future.result()

    return {"image": image_desc, "audio": transcript}

Problema 2: Costo alto en pipelines complejos

Causa: Usar gpt-4o para todo.

Solución: Usar modelos económicos para pasos intermedios:

  • Clasificación/routing → gpt-4o-mini
  • Análisis profundo → gpt-4o
  • Transcripción → whisper-1

Problema 3: Resultado inconsistente entre ejecuciones

Causa: temperature > 0 en pasos de extracción.

Solución: Usar temperature=0 en pasos que requieren determinismo (extracción JSON, clasificación).


Ejercicios

Ejercicio 1: Pipeline imagen → descripción → audio (Fácil)

Crea un pipeline que: recibe imagen → la describe con vision → genera audio de la descripción.

Ver solución
def image_to_audio_description(image_path: str) -> dict:
    # 1. Describir imagen
    description = vision_plus_llm(
        image_path,
        "Describe esta imagen en 3 oraciones claras y concisas."
    )

    # 2. Generar audio
    response = client.audio.speech.create(
        model="tts-1",
        voice="nova",
        input=description
    )
    audio_path = "image_description.mp3"
    response.stream_to_file(audio_path)

    return {
        "description": description,
        "audio": audio_path
    }

Explicación: Combina visión (cápsula 02) con TTS (cápsula 03). Útil para accesibilidad.

Ejercicio 2: Router de modalidad (Medio)

Crea una función que detecte si un archivo es imagen, audio o texto, y aplique el procesamiento correcto.

Ver solución
from pathlib import Path

IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mp4", ".webm"}
TEXT_EXTENSIONS = {".txt", ".md", ".csv"}

def route_and_process(file_path: str, prompt: str = "Analiza este contenido.") -> dict:
    ext = Path(file_path).suffix.lower()

    if ext in IMAGE_EXTENSIONS:
        result = vision_plus_llm(file_path, prompt)
        return {"modality": "image", "result": result}

    elif ext in AUDIO_EXTENSIONS:
        result = audio_to_analysis(file_path, prompt)
        return {"modality": "audio", "result": result}

    elif ext in TEXT_EXTENSIONS:
        with open(file_path, "r") as f:
            text = f.read()
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": text}
            ],
            max_tokens=500
        )
        return {"modality": "text", "result": response.choices[0].message.content}

    else:
        return {"modality": "unknown", "result": f"Formato no soportado: {ext}"}

Explicación: Este es el core del Clasificador multimodal (cápsula 08). Detecta modalidad por extensión y aplica el pipeline correcto.

Ejercicio 3: Pipeline con fallback (Medio)

Crea un pipeline de análisis de imagen que intente GPT-4o primero, y si falla (error, timeout), use gpt-4o-mini como fallback.

Ver solución
import time

def analyze_with_fallback(image_path: str, prompt: str) -> dict:
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode()

    models = [("gpt-4o", "primary"), ("gpt-4o-mini", "fallback")]

    for model, tier in models:
        try:
            start = time.time()
            response = client.chat.completions.create(
                model=model,
                messages=[{
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {"type": "image_url", "image_url": {
                            "url": f"data:image/jpeg;base64,{image_data}"
                        }}
                    ]
                }],
                max_tokens=500,
                timeout=30
            )
            return {
                "model_used": model,
                "tier": tier,
                "result": response.choices[0].message.content,
                "latency": round(time.time() - start, 2)
            }
        except Exception as e:
            if tier == "fallback":
                raise RuntimeError(f"Todos los modelos fallaron: {e}")
            continue

Explicación: El patrón de fallback es fundamental para producción. Si el modelo principal falla, el sistema sigue funcionando con un modelo más simple.

Ejercicio 4: Pipeline multi-input (Difícil)

Crea una función que acepte una lista de archivos (mezclando imágenes, audios y textos) y devuelva un análisis integrado de todos.

Ver solución
def analyze_multiple_inputs(file_paths: list[str]) -> dict:
    contexts = []

    for path in file_paths:
        ext = Path(path).suffix.lower()

        if ext in IMAGE_EXTENSIONS:
            desc = vision_plus_llm(path, "Describe esta imagen en 2 frases.")
            contexts.append(f"[Imagen: {Path(path).name}] {desc}")

        elif ext in AUDIO_EXTENSIONS:
            with open(path, "rb") as f:
                text = client.audio.transcriptions.create(
                    model="whisper-1", file=f
                ).text
            contexts.append(f"[Audio: {Path(path).name}] {text}")

        elif ext in TEXT_EXTENSIONS:
            with open(path, "r") as f:
                text = f.read()[:2000]  # Limitar largo
            contexts.append(f"[Texto: {Path(path).name}] {text}")

    combined = "\n\n".join(contexts)

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": (
                f"Analiza toda esta información y genera:\n"
                f"1. Resumen integrado\n"
                f"2. Conexiones entre los diferentes inputs\n"
                f"3. Insights principales\n\n{combined}"
            )
        }],
        max_tokens=500
    )

    return {
        "num_inputs": len(file_paths),
        "modalities": [Path(p).suffix for p in file_paths],
        "analysis": response.choices[0].message.content
    }

Explicación: Este patrón es la base de sistemas de análisis multimodal completos. Procesa cada input según su modalidad y luego integra todo en un solo análisis.


Resumen

En esta cápsula aprendiste:

  • Las combinaciones multimodales pueden ser por input (múltiples modalidades al mismo modelo), por pipeline (encadenar modelos), o por output (generar en múltiples modalidades)
  • Los 6 patrones principales: Vision+LLM, Audio→Texto→LLM, Texto→LLM→Imagen, Documento→Extracción, Pipeline completo, Generación→Análisis
  • Los principios de diseño: cada paso con propósito claro, usar modelo más barato posible, manejar errores por paso, medir latencia
  • Paralelizar pasos independientes reduce latencia significativamente
  • El router de modalidad (detectar tipo → aplicar pipeline) es el core del Clasificador multimodal

Próxima cápsula: Landscape de modelos — comparación detallada de GPT-4V vs Claude 3 vs Gemini.


Recursos Adicionales

  1. OpenAI API Reference — Referencia completa de todas las APIs
  2. LangChain Multimodal — Pipelines multimodales con LangChain
  3. OpenAI Cookbook — Ejemplos prácticos de combinaciones
  4. asyncio Documentation — Para paralelizar pipelines
  5. concurrent.futures — Threading para I/O-bound tasks
  6. Building Multimodal AI Applications — Curso de DeepLearning.AI