Módulo 7: Casos de Uso
5. Combinaciones Multi-Modalidad
Descripción
Las cápsulas anteriores cubrieron patrones individuales: Document Q&A, Image Analysis, Video Frames. Pero el verdadero poder de la IA multimodal aparece cuando combinas modalidades en un solo pipeline: audio de una reunión + diapositivas en PDF → acta integrada; documento + pregunta de voz → respuesta en audio; imagen + texto + audio → análisis unificado.
Por qué importa: En producción, rara vez usas una sola modalidad. Un sistema de meeting notes combina transcripción de audio, análisis de presentaciones, y generación textual. Un asistente médico combina imágenes de radiografías, notas del doctor (audio), e historial clínico (texto). Un sistema educativo combina video de clase, slides, y transcripción para generar materiales de estudio. Dominar combinaciones es lo que diferencia un script con una API de un sistema multimodal real.
Conexión con el módulo: Esta cápsula integra lo que viste en cápsulas 02-04 y conecta directamente con el Use Case Selector (cápsula 08), que necesita detectar inputs multi-modales y combinar pipelines.
Taxonomía de Combinaciones
Por tipo de integración
| Tipo | Descripción | Ejemplo |
|---|---|---|
| Input multi-modal | Múltiples modalidades como input a un solo modelo | Imagen + pregunta → GPT-4o → respuesta |
| Pipeline secuencial | Salida de un modelo como entrada de otro | Audio → Whisper → texto → GPT-4o → resumen → TTS → audio |
| Pipeline paralelo | Procesar modalidades en paralelo y combinar | Audio → transcripción ∥ PDF → extracción → combinar → LLM |
| Output multi-modal | Un pipeline genera outputs en varias modalidades | Documento → análisis textual + gráfico + resumen de audio |
Los 5 patrones más usados
1. Reunión + Documento → Acta integrada
2. Imagen + Pregunta → Respuesta contextual
3. Documento → Texto + Imagen + Audio (output multi-modal)
4. Video + Audio → Análisis completo
5. Tri-modal: Texto + Imagen + Audio → Respuesta unificada
Patrón 1: Reunión + Documento
Un equipo tiene una reunión donde discuten un documento (contrato, propuesta, reporte). El sistema toma la grabación de la reunión + el PDF del documento y genera un acta que conecta ambos.
Pipeline visual
┌─────────────┐ ┌─────────────┐
│ Audio │──▶ Whisper ──▶ │ │
│ reunión │ Transcr. │ Combinar │
└─────────────┘ │ contextos │──▶ LLM ──▶ Acta
┌─────────────┐ │ │ integrada
│ PDF │──▶ PyMuPDF ──▶ │ │
│ documento │ Texto+imgs └─────────────┘
└─────────────┘
Implementación completa
from openai import OpenAI
import fitz
import base64
client = OpenAI()
def transcribe_audio(audio_path: str) -> str:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="text"
)
return response
def extract_text_from_pdf(pdf_path: str) -> str:
doc = fitz.open(pdf_path)
text = "\n\n".join(doc[i].get_text() for i in range(len(doc)))
doc.close()
return text
def extract_images_from_pdf(pdf_path: str) -> list[str]:
doc = fitz.open(pdf_path)
image_descriptions = []
for page_num in range(len(doc)):
page = doc[page_num]
images = page.get_images(full=True)
for img_info in images:
xref = img_info[0]
base_image = doc.extract_image(xref)
b64 = base64.b64encode(base_image["image"]).decode()
desc_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe esta imagen/diagrama en 2-3 oraciones."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]
}],
max_tokens=150
)
image_descriptions.append(
f"[Imagen p.{page_num + 1}]: {desc_response.choices[0].message.content}"
)
doc.close()
return image_descriptions
def meeting_with_document(audio_path: str, doc_path: str) -> dict:
transcript = transcribe_audio(audio_path)
doc_text = extract_text_from_pdf(doc_path)
doc_images = extract_images_from_pdf(doc_path)
images_text = "\n".join(doc_images) if doc_images else "Sin imágenes."
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"Genera un acta de reunión profesional. "
"Conecta lo discutido en la reunión con el contenido del documento. "
"Estructura: Resumen ejecutivo, Puntos discutidos, Decisiones tomadas, "
"Acciones pendientes, Referencias al documento."
)
},
{
"role": "user",
"content": (
f"TRANSCRIPCIÓN DE LA REUNIÓN:\n{transcript[:6000]}\n\n"
f"CONTENIDO DEL DOCUMENTO:\n{doc_text[:4000]}\n\n"
f"IMÁGENES/DIAGRAMAS DEL DOCUMENTO:\n{images_text[:2000]}"
)
}
],
max_tokens=1000,
temperature=0.3
)
return {
"minutes": response.choices[0].message.content,
"transcript_length": len(transcript),
"document_pages": doc_text.count("\n\n"),
"images_analyzed": len(doc_images)
}
Uso:
result = meeting_with_document(
"reunion_equipo.mp3",
"propuesta_proyecto.pdf"
)
print(result["minutes"])
Patrón 2: Imagen + Pregunta Contextual
Más allá de "describe esta imagen", el usuario da contexto textual que modifica cómo interpretar la imagen.
def contextual_image_qa(
image_path: str,
question: str,
context: str = ""
) -> dict:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
prompt = question
if context:
prompt = f"Contexto: {context}\n\nPregunta: {question}"
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]
}],
max_tokens=500,
temperature=0.2
)
return {
"answer": response.choices[0].message.content,
"context_used": bool(context)
}
Con historial de conversación
class ImageConversation:
def __init__(self, image_path: str):
self.image_path = image_path
with open(image_path, "rb") as f:
self.image_b64 = base64.b64encode(f.read()).decode()
self.messages: list[dict] = []
def ask(self, question: str) -> str:
if not self.messages:
self.messages.append({
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{self.image_b64}"}}
]
})
else:
self.messages.append({"role": "user", "content": question})
response = client.chat.completions.create(
model="gpt-4o",
messages=self.messages,
max_tokens=500
)
answer = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": answer})
return answer
Patrón 3: Output Multi-Modal (Documento → Texto + Imagen + Audio)
Un documento de entrada genera tres outputs diferentes:
def multimodal_output_pipeline(doc_path: str) -> dict:
doc_text = extract_text_from_pdf(doc_path)
summary_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Resume este documento en 3-5 puntos clave:\n\n{doc_text[:6000]}"
}],
max_tokens=400
)
text_summary = summary_response.choices[0].message.content
viz_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": (
f"Basándote en este resumen, genera un prompt para DALL-E "
f"que ilustre el concepto principal como infografía profesional:\n\n{text_summary}"
)
}],
max_tokens=200
)
image_prompt = viz_response.choices[0].message.content
image_response = client.images.generate(
model="dall-e-3",
prompt=image_prompt,
size="1024x1024",
n=1
)
image_url = image_response.data[0].url
audio_response = client.audio.speech.create(
model="tts-1",
voice="nova",
input=text_summary
)
audio_path = "/tmp/document_summary.mp3"
audio_response.stream_to_file(audio_path)
return {
"text_summary": text_summary,
"image_url": image_url,
"audio_path": audio_path
}
Patrón 4: Video + Audio Completo
Combina el análisis visual de frames (cápsula 04) con la transcripción del audio del video:
import subprocess
def extract_audio_from_video(video_path: str, output_path: str = "/tmp/video_audio.mp3") -> str:
subprocess.run([
"ffmpeg", "-i", video_path, "-vn", "-acodec", "libmp3lame",
"-y", output_path
], capture_output=True, check=True)
return output_path
def full_video_analysis(video_path: str, target_frames: int = 15) -> dict:
audio_path = extract_audio_from_video(video_path)
transcript = transcribe_audio(audio_path)
import cv2
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps
interval = duration / target_frames
frames_descriptions = []
for i in range(target_frames):
ts = i * interval
cap.set(cv2.CAP_PROP_POS_FRAMES, int(ts * fps))
ret, frame = cap.read()
if not ret:
break
frame_path = f"/tmp/full_analysis_frame_{i}.jpg"
cv2.imwrite(frame_path, frame)
with open(frame_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe esta escena en 1-2 oraciones."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]
}],
max_tokens=100
)
minutes = int(ts // 60)
seconds = int(ts % 60)
frames_descriptions.append(f"{minutes}:{seconds:02d} - {response.choices[0].message.content}")
cap.release()
visual_timeline = "\n".join(frames_descriptions)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": (
"Genera un análisis completo de este video integrando lo visual con lo hablado.\n\n"
f"ANÁLISIS VISUAL (frames):\n{visual_timeline}\n\n"
f"TRANSCRIPCIÓN DEL AUDIO:\n{transcript[:5000]}\n\n"
"Estructura: Resumen, Timeline con momentos clave, Temas principales, Conclusiones."
)
}],
max_tokens=800
)
return {
"integrated_analysis": response.choices[0].message.content,
"visual_frames": len(frames_descriptions),
"transcript_length": len(transcript),
"duration_seconds": round(duration, 2)
}
Patrón 5: Tri-Modal (Texto + Imagen + Audio)
El usuario envía tres inputs simultáneos y el sistema integra todo:
def tri_modal_analysis(
text_input: str,
image_path: str,
audio_path: str
) -> dict:
transcript = transcribe_audio(audio_path)
with open(image_path, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{
"type": "text",
"text": (
"Analiza estos tres inputs y genera una respuesta integrada:\n\n"
f"TEXTO DEL USUARIO:\n{text_input}\n\n"
f"AUDIO TRANSCRITO:\n{transcript}\n\n"
"IMAGEN ADJUNTA (ver abajo):\n"
"Conecta la información de las tres fuentes en una respuesta coherente."
)
},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}
}
]
}],
max_tokens=600,
temperature=0.3
)
return {
"integrated_response": response.choices[0].message.content,
"inputs": {
"text_length": len(text_input),
"transcript_length": len(transcript),
"image_path": image_path
}
}
Pipeline Genérico Multi-Modal
Para no repetir código, un pipeline configurable:
class MultiModalPipeline:
def __init__(self):
self.processors = {
"text": self._process_text,
"image": self._process_image,
"audio": self._process_audio,
"pdf": self._process_pdf
}
def process(self, inputs: dict[str, str], task: str = "analyze") -> dict:
processed = {}
for input_type, input_path in inputs.items():
if input_type in self.processors:
processed[input_type] = self.processors[input_type](input_path)
return self._combine(processed, task)
def _process_text(self, text: str) -> str:
return text
def _process_image(self, path: str) -> str:
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = 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,{b64}"}}
]
}],
max_tokens=300
)
return response.choices[0].message.content
def _process_audio(self, path: str) -> str:
return transcribe_audio(path)
def _process_pdf(self, path: str) -> str:
return extract_text_from_pdf(path)
def _combine(self, processed: dict, task: str) -> dict:
context_parts = []
for input_type, content in processed.items():
context_parts.append(f"[{input_type.upper()}]:\n{content[:3000]}")
context = "\n\n".join(context_parts)
task_prompts = {
"analyze": "Analiza toda la información y genera un resumen integrado.",
"compare": "Compara la información de las diferentes fuentes.",
"summarize": "Resume los puntos clave de todas las fuentes.",
"qa": "Responde cualquier pregunta implícita en los inputs."
}
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"{context}\n\n{task_prompts.get(task, task_prompts['analyze'])}"
}],
max_tokens=600
)
return {
"result": response.choices[0].message.content,
"inputs_processed": list(processed.keys()),
"task": task
}
Uso:
pipeline = MultiModalPipeline()
result = pipeline.process(
inputs={
"audio": "reunion.mp3",
"pdf": "propuesta.pdf",
"image": "diagrama.png"
},
task="summarize"
)
print(result["result"])
Troubleshooting
Problema 1: Token limit excedido al combinar modalidades
Síntoma: Error maximum context length exceeded al enviar transcripción + documento + imagen.
Solución:
def truncate_inputs(inputs: dict[str, str], max_total_chars: int = 15000) -> dict[str, str]:
total = sum(len(v) for v in inputs.values())
if total <= max_total_chars:
return inputs
per_input = max_total_chars // len(inputs)
return {k: v[:per_input] for k, v in inputs.items()}
Problema 2: Audio largo (>25 MB) no se puede transcribir
Síntoma: Whisper rechaza archivos mayores a 25 MB.
Solución: Dividir el audio en segmentos:
from pydub import AudioSegment
def transcribe_long_audio(audio_path: str, segment_minutes: int = 10) -> str:
audio = AudioSegment.from_file(audio_path)
segment_ms = segment_minutes * 60 * 1000
segments = [audio[i:i + segment_ms] for i in range(0, len(audio), segment_ms)]
transcripts = []
for i, segment in enumerate(segments):
segment_path = f"/tmp/audio_segment_{i}.mp3"
segment.export(segment_path, format="mp3")
transcript = transcribe_audio(segment_path)
transcripts.append(transcript)
return "\n\n".join(transcripts)
Problema 3: Resultados incoherentes al combinar fuentes
Síntoma: El resumen no conecta bien audio con documento.
Solución: Usar un prompt más estructurado:
system_prompt = (
"Integra información de múltiples fuentes. Para cada punto, indica de qué fuente proviene. "
"Si hay contradicciones entre fuentes, señálalas explícitamente. "
"Estructura tu respuesta con: 1) Puntos en común, 2) Información única de cada fuente, "
"3) Contradicciones (si las hay), 4) Conclusión integrada."
)
Problema 4: Latencia alta en pipelines multi-paso
Síntoma: El pipeline tarda 30+ segundos.
Solución: Paralelizar pasos independientes:
import asyncio
from openai import AsyncOpenAI
async def process_inputs_parallel(audio_path: str, doc_path: str, image_path: str) -> dict:
aclient = AsyncOpenAI()
async def transcribe_async():
with open(audio_path, "rb") as f:
return await aclient.audio.transcriptions.create(model="whisper-1", file=f, response_format="text")
async def extract_doc():
return extract_text_from_pdf(doc_path)
async def describe_image():
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = await aclient.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe esta imagen."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]
}],
max_tokens=200
)
return response.choices[0].message.content
transcript, doc_text, img_desc = await asyncio.gather(
transcribe_async(), extract_doc(), describe_image()
)
return {"transcript": transcript, "doc_text": doc_text, "image_description": img_desc}
Ejercicios
Ejercicio 1: Pipeline documento → imagen → análisis
Extrae la primera imagen de un PDF, genera una variación con DALL-E 3, y compara ambas con Vision.
Ver solución
def doc_image_variation_pipeline(pdf_path: str) -> dict:
doc = fitz.open(pdf_path)
first_image = None
for page_num in range(len(doc)):
images = doc[page_num].get_images(full=True)
if images:
xref = images[0][0]
base_image = doc.extract_image(xref)
first_image = base_image["image"]
break
doc.close()
if not first_image:
return {"error": "No se encontraron imágenes en el PDF"}
b64_original = base64.b64encode(first_image).decode()
desc_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe esta imagen para poder recrearla con DALL-E."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_original}"}}
]
}],
max_tokens=200
)
description = desc_response.choices[0].message.content
variation = client.images.generate(
model="dall-e-3",
prompt=f"Versión mejorada y profesional de: {description}",
size="1024x1024",
n=1
)
return {
"original_description": description,
"variation_url": variation.data[0].url,
"source_pdf": pdf_path
}
Ejercicio 2: Sistema de meeting notes completo
Extiende meeting_with_document para que genere: acta (texto), resumen ejecutivo (texto corto), y resumen en audio (TTS).
Ver solución
def complete_meeting_notes(audio_path: str, doc_path: str) -> dict:
base_result = meeting_with_document(audio_path, doc_path)
minutes = base_result["minutes"]
exec_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Genera un resumen ejecutivo de 3 líneas de esta acta:\n\n{minutes}"
}],
max_tokens=150
)
executive_summary = exec_response.choices[0].message.content
audio_response = client.audio.speech.create(
model="tts-1",
voice="nova",
input=executive_summary
)
audio_output = "/tmp/meeting_summary.mp3"
audio_response.stream_to_file(audio_output)
return {
"full_minutes": minutes,
"executive_summary": executive_summary,
"audio_summary_path": audio_output
}
Ejercicio 3: Tri-modal con detección automática
Crea una función que reciba una lista de paths, detecte automáticamente el tipo de cada uno (texto, imagen, audio, PDF), y aplique el pipeline multi-modal correspondiente.
Ver solución
from pathlib import Path
def auto_multimodal_pipeline(paths: list[str], task: str = "analyze") -> dict:
type_map = {
".pdf": "pdf",
".png": "image", ".jpg": "image", ".jpeg": "image", ".webp": "image",
".mp3": "audio", ".wav": "audio", ".m4a": "audio",
".txt": "text", ".md": "text"
}
inputs = {}
for path in paths:
ext = Path(path).suffix.lower()
input_type = type_map.get(ext)
if input_type:
if input_type == "text":
with open(path, "r") as f:
inputs[input_type] = f.read()
else:
inputs[input_type] = path
if not inputs:
return {"error": "No se detectaron inputs válidos"}
pipeline = MultiModalPipeline()
return pipeline.process(inputs, task)
Recursos Adicionales
- OpenAI Audio API — Whisper y TTS
- OpenAI Vision — Multi-imagen en un request
- DALL-E 3 API — Generación de imágenes
- FFmpeg Documentation — Extracción de audio de video