- Introducción a la Inteligencia Artificial Multimodal
La Inteligencia Artificial Multimodal (IAM) se refiere a la capacidad de los modelos de IA para procesar y generar simultáneamente múltiples tipos de datos, como texto, imágenes, audio y video. A diferencia de los modelos puramente textuales, los sitsemas multimodales pueden llevar a cabo tareas complejas como la comprensión visual, la respuesta a preguntas basadas en imágenes y la interacción por voz. Esto representa un avance crucial en la evolución de la IA, pasando de una capacidad puramente "lingüística" a una que también "ve y oye".
- Modelos Multimodales Destacados
La siguiente tabla compara algunas de las plataformas de IA multimodal más conocidas, destacando sus capacidades en diferentes dominios y su disponibilidad.
Modelo Comprensión Visual Procesamiento Voz Comprensión Video Costo API (aprox.) Código Abierto
-----------------------------------------------------------------------------------------------------------------
GPT-4o Alto Medio Bajo Medio No
Claude 3.5 Sonnet Alto Bajo Bajo Medio No
Gemini 1.5 Pro Alto Alto Alto Bajo No
Qwen-VL Alto Bajo Bajo Bajo Sí
InternVL Alto Bajo Bajo Bajo Sí
LLaVA Medio Bajo Bajo Gratis Sí
- Implementación de Comprensión Visual con GPT-4o
A continuación, se muestra cómo integrar la API de GPT-4o para aálisis de imágenes en una aplicación Spring Boot, utilizando WebClient para realizar las solicitudes HTTP de manera reactiva.
3.1. Servicio de Análisis de Imágenes
package com.example.multimodal.services;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
@Service
public class VisionProcessingService {
private final WebClient webClient;
private final String openAiApiKey;
private static final String GPT4O_VISION_ENDPOINT = "https://api.openai.com/v1/chat/completions";
public VisionProcessingService(WebClient.Builder webClientBuilder, @Value("${openai.api.key}") String openAiApiKey) {
this.openAiApiKey = openAiApiKey;
this.webClient = webClientBuilder.baseUrl(GPT4O_VISION_ENDPOINT).build();
}
public Mono<String> analyzeImageWithGPT4o(String imageUrl, String queryText) {
Map<String, Object> imageContent = Map.of(
"type", "image_url",
"image_url", Map.of("url", imageUrl, "detail", "high")
);
Map<String, Object> textContent = Map.of(
"type", "text",
"text", queryText
);
Map<String, Object> message = Map.of(
"role", "user",
"content", List.of(textContent, imageContent)
);
Map<String, Object> requestBody = Map.of(
"model", "gpt-4o",
"max_tokens", 1000,
"messages", List.of(message)
);
return webClient.post()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(Map.class)
.map(response -> {
List<Map> choices = (List<Map>) response.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> firstChoice = choices.get(0);
Map<String, Object> msg = (Map<String, Object>) firstChoice.get("message");
return (String) msg.get("content");
}
return "No se pudo obtener una respuesta del modelo.";
});
}
}
3.2. Controlador de API
package com.example.multimodal.controllers;
import com.example.multimodal.services.VisionProcessingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.Base64;
import java.util.Map;
@RestController
@RequestMapping("/api/multimodal-vision")
public class VisionController {
@Autowired
private VisionProcessingService visionProcessingService;
@GetMapping("/analyze-url")
public Mono<Map<String, String>> analyzeImageUrl(
@RequestParam String imageUrl,
@RequestParam(defaultValue = "Describe esta imagen.") String question) {
return visionProcessingService.analyzeImageWithGPT4o(imageUrl, question)
.map(result -> Map.of("description", result));
}
@PostMapping(value = "/analyze-upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<Map<String, String>> uploadAndAnalyzeImage(
@RequestPart("file") MultipartFile file,
@RequestParam(defaultValue = "Qué se ve en la imagen?") String question) throws IOException {
String base64Image = Base64.getEncoder().encodeToString(file.getBytes());
String mimeType = file.getContentType();
String dataUri = "data:" + mimeType + ";base64," + base64Image;
return visionProcessingService.analyzeImageWithGPT4o(dataUri, question)
.map(result -> Map.of("analysisResult", result, "fileName", file.getOriginalFilename()));
}
}
- Integración de Visión con Claude 3.5 Sonnet
El siguiente servicio demuestra cómo interactuar con la API de Anthropic Claude para análisis de imágenes, siguiendo un patrón similar al de OpenAI pero adaptado a la estructura de solicitud de Claude.
package com.example.multimodal.services;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
@Service
public class ClaudeVisionAnalyzer {
private final WebClient claudeWebClient;
private final String anthropicApiKey;
private static final String CLAUDE_MESSAGES_ENDPOINT = "https://api.anthropic.com/v1/messages";
public ClaudeVisionAnalyzer(WebClient.Builder webClientBuilder, @Value("${anthropic.api.key}") String anthropicApiKey) {
this.anthropicApiKey = anthropicApiKey;
this.claudeWebClient = webClientBuilder.baseUrl(CLAUDE_MESSAGES_ENDPOINT).build();
}
public Mono<String> queryImageWithClaude(String imageUrl, String textPrompt) {
Map<String, Object> imageSource = Map.of(
"type", "url",
"media_type", "image/jpeg", // Asumiendo JPEG, ajustar si es necesario
"data", imageUrl
);
Map<String, Object> imageBlock = Map.of(
"type", "image",
"source", imageSource
);
Map<String, Object> textBlock = Map.of(
"type", "text",
"text", textPrompt
);
Map<String, Object> messageContent = Map.of(
"role", "user",
"content", List.of(imageBlock, textBlock)
);
Map<String, Object> requestBody = Map.of(
"model", "claude-3-5-sonnet-20241022",
"max_tokens", 1024,
"messages", List.of(messageContent)
);
return claudeWebClient.post()
.header("x-api-key", anthropicApiKey)
.header("anthropic-version", "2023-06-01")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(Map.class)
.map(response -> {
List<Map> content = (List<Map>) response.get("content");
if (content != null && !content.isEmpty()) {
Map<String, Object> firstContentBlock = content.get(0);
return (String) firstContentBlock.get("text");
}
return "No se obtuvo respuesta de Claude.";
});
}
}
- Módulos de Interacción por Voz (Whisper y TTS)
Para la interacción por voz, se utilizan dos componentes clave de OpenAI: Whisper para la transcripción de audio a texto y la API de Texto a Voz (TTS) para convertir texto en audio.
5.1. Transcripción de Audio (Speech-to-Text con Whisper)
package com.example.multimodal.services;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.Map;
@Service
public class AudioTranscriptionService {
private final WebClient openAiAudioWebClient;
private final String openAiApiKey;
private static final String WHISPER_ENDPOINT = "https://api.openai.com/v1/audio/transcriptions";
public AudioTranscriptionService(WebClient.Builder webClientBuilder, @Value("${openai.api.key}") String openAiApiKey) {
this.openAiApiKey = openAiApiKey;
this.openAiAudioWebClient = webClientBuilder.baseUrl(WHISPER_ENDPOINT).build();
}
public Mono<String> transcribeAudio(MultipartFile audioFile) throws IOException {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new ByteArrayResource(audioFile.getBytes()) {
@Override
public String getFilename() {
return audioFile.getOriginalFilename();
}
});
body.add("model", "whisper-1");
body.add("language", "es"); // Especificar español
return openAiAudioWebClient.post()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(body))
.retrieve()
.bodyToMono(Map.class)
.map(response -> (String) response.get("text"));
}
}
5.2. Conversión de Texto a Voz (Text-to-Speech con TTS)
package com.example.multimodal.services;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
@Service
public class TextToSpeechService {
private final WebClient openAiTtsWebClient;
private final String openAiApiKey;
private static final String TTS_ENDPOINT = "https://api.openai.com/v1/audio/speech";
public TextToSpeechService(WebClient.Builder webClientBuilder, @Value("${openai.api.key}") String openAiApiKey) {
this.openAiApiKey = openAiApiKey;
this.openAiTtsWebClient = webClientBuilder.baseUrl(TTS_ENDPOINT).build();
}
public Mono<byte[]> convertTextToAudio(String textContent) {
Map<String, Object> requestBody = Map.of(
"model", "tts-1",
"input", textContent,
"voice", "alloy" // Voces disponibles: 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'
);
return openAiTtsWebClient.post()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(byte[].class);
}
}
- Caso Práctico: Asistente Multimodal de Atención al Cliente
Este ejemplo integra los servicios de transcripción de voz, análisis visual y generación de texto/voz para crear un sistema de atención al cliente que puede comprender y responder a entradas tanto de audio como de imagan.
package com.example.multimodal.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.Base64;
import java.util.Objects;
@Service
public class MultimodalCustomerAssistant {
private static final Logger log = LoggerFactory.getLogger(MultimodalCustomerAssistant.class);
@Autowired private AudioTranscriptionService audioTranscriptionService;
@Autowired private VisionProcessingService visionProcessingService;
@Autowired private TextToSpeechService textToSpeechService;
public Mono<String> processCustomerInteraction(MultipartFile audioInput, MultipartFile imageInput) throws IOException {
Mono<String> audioTextMono = Mono.just("No se proporcionó entrada de audio.");
if (audioInput != null && !audioInput.isEmpty()) {
audioTextMono = audioTranscriptionService.transcribeAudio(audioInput)
.doOnNext(text -> log.info("Usuario dice: {}", text));
}
Mono<String> imageAnalysisMono = Mono.just("No se proporcionó imagen.");
if (imageInput != null && !imageInput.isEmpty()) {
String base64Image = Base64.getEncoder().encodeToString(imageInput.getBytes());
String mimeType = Objects.requireNonNull(imageInput.getContentType());
String dataUri = "data:" + mimeType + ";base64," + base64Image;
imageAnalysisMono = visionProcessingService.analyzeImageWithGPT4o(dataUri, "Analiza el problema o producto en esta imagen.")
.doOnNext(analysis -> log.info("Análisis de imagen: {}", analysis));
}
return Mono.zip(audioTextMono, imageAnalysisMono)
.flatMap(tuple -> {
String userVoiceInput = tuple.getT1();
String imageDescription = tuple.getT2();
String combinedPrompt = String.format(
"Entrada de voz del usuario: %s\nResultado del análisis de imagen: %s\nProporciona una respuesta profesional de atención al cliente:",
userVoiceInput, imageDescription
);
// Nota: La llamada a analyzeImageWithGPT4o aquí requiere una URL o data URI de imagen.
// Para una consulta puramente textual, OpenAI GPT-4o también puede ser usado sin imagen,
// pero la firma del método actual requiere una. Se podría modificar el servicio para
// aceptar un indicador de "solo texto" o usar un endpoint de solo texto.
// Aquí, usamos una imagen dummy para cumplir la firma, pero en un caso real se ajustaría.
String dummyImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
return visionProcessingService.analyzeImageWithGPT4o(dummyImage, combinedPrompt)
.doOnNext(reply -> log.info("Respuesta del asistente: {}", reply))
.flatMap(textToSpeechService::convertTextToAudio) // Convierte la respuesta a audio
.map(audioBytes -> Base64.getEncoder().encodeToString(audioBytes)); // Retorna el audio en Base64
});
}
}
- Recomendaciones para Mejores Prácticas
- Optimización de Imágenes: Comprime las imágenes antes de enviarlas a las APIs de visión para reducir el consumo de tokens y acelerar las respuestas.
- Respuestas en Streaming: Para respuestas textuales extensas, considera implementar Server-Sent Events (SSE) para una experiencia de usuario más fluida.
- Fusión Multimodal: La combinación de múltiples modalidades (como voz e imagen) en una sola consulta a menudo produce resultados más precisos y contextualmente ricos.
- Estrategias de Caché: Almacena en caché los resultados de análisis de imágenes idénticas utilizando una base de datos o Redis para minimizar llamadas redundantes a la API y reducir costos.
- Manejo de Errores y Alternativas: Implementa mecanismos robustos para manejar fallos de API, límites de tasa o tiempos de espera. Considera la degradación a un modo puramente textual o el uso de un modelo más simple como alternativa.