Meeting Analysis: un proyecto de análisis inteligente de reuniones usando Spring Boot y Whisper

Aprende a crear con Java y Spring Boot una app que transcribe y resume reuniones en video usando Whisper y Ollama AI. Análisis inteligente paso a paso.

Meeting Analysis: un proyecto de análisis inteligente de reuniones usando Spring Boot y Whisper

En reuniones largas, muchas ideas importantes se pierden entre horas de conversación. Me propuse crear una herramienta que pudiera tomar un video de una reunión, transcribirlo y resumir los puntos clave automáticamente. Así nació Meeting Analysis. Tendremos dos endpoints: uno para subir transcripciones directamente, y otro para subir un video desde el cual se extraerá y transcribirá el audio automáticamente, posteriormente para ser analizado por Ollama.


📚 Tabla de Contenidos


Requisitos previos

  • Java 17+
  • Maven o Gradle
  • Spring Boot (versión 3.x recomendada)
  • Python 3.8+
  • ffmpeg
  • Whisper
  • Acceso a Ollama.

Crear proyecto Spring Boot

Crea un proyecto Spring Boot usando Spring Initializr (https://start.spring.io) con las siguientes dependencias:

  • Spring Web
  • Spring AI Ollama
  • Lombok
  • Spring DebTools (opcional)

Tendrias que tener las siguientes dependencias:

	dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }

Estructura de carpetas

Dentro de src/main/java/com/tuempresa/meetinganalysis/, crea los paquetes y clases:


src/
└── main/
    ├── java/com/tuempresa/meetinganalysis/
    │   ├── controller/
    │   │   └── MeetingController.java
    │   ├── dto/
    │   │   ├── MeetingAnalysisRequest.java
    │   │   └── MeetingAnalysisResponse.java
    │   ├── prompt/
    │   │   └── PromptBuilder.java
    │   └── service/
    │       ├── AudioExtractorService.java
    │       ├── AudioTranscriptionService.java
    │       ├── MeetingService.java
    │       └── MeetingServiceFile.java
    └── resources/
        └── scripts/
            └── transcribe.py

Además en resources creamos una carpeta scripts en el cual creamos el siguiente archivo .py


resources
└─scripts/
  └─ transcribe.py

Creación de Dto

MeetingAnalysisRequest

Este record va a ser el modelo que recibe la transcripción.


public record MeetingAnalysisRequest(String transcript) {
}

MeetingAnalysisResponse

Este record es el formato de la respuesta que vamos a obtener una vez se procese el texto por Ollama


public record MeetingAnalysisResponse(
        String summary,
        List<String> task,
        List<String> decisions,
        List<String> openQuestions
) {
}

Implementando el PromptBuilder

La clase PromptBuilder sirve para construir un mensaje (prompt) que será enviado a un modelo de lenguaje (como Ollama 3.5) para analizar una transcripción de una reunión. Es una clase de Spring anotoda con @component, lo que significa que es un bean gestionado por el contenedor de Spring.


import org.springframework.stereotype.Component;

@Component
public class PromptBuilder {
    public String build(String transcript){
        return """
                Eres un asistente experto en análisis de reuniones. Dada la siguiente transcripción,
                analiza y responde estrictamente con un JSON válido en el siguiente formato, **sin texto adicional ni explicaciones**:
                
                {
                  "summary": "Resumen breve",
                  "tasks": ["Tarea 1", "Tarea 2"],
                  "decisions": ["Decisión A", "Decisión B"],
                  "openQuestions": ["¿Qué pasa con X?", "¿Quién hará Y?"]
                }
                
                La transcripción es:
                %s
                
                Recuerda, responde únicamente con el JSON válido y nada más.
                """.formatted(transcript);
    }
}

Si nos fijamos en nuestro prompt vamos a obtener una respuesta al record MeetingAnalysysResponse.java que creamos anteriormente.

Implementando los servicios

MeeetingService

La clase MeetingService es una clase de servicio en un proyecto Spring cuyo propósito principal es analizar una transcripción de reunión usando un modelo de lenguaje (IA) en este caso Ollama y devolver los resultados estructurados.


import com.amoelcodigo.meetinganalysis.dto.MeetingAnalysisResponse;
import com.amoelcodigo.meetinganalysis.prompt.PromptBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
public class MeetingService {

    private final OllamaChatModel ollamaChatModel;
    private final PromptBuilder promptBuilder;
    private final ObjectMapper objectMapper;

    public MeetingAnalysisResponse analyzeMeeting(String transcript) {
        String prompt = promptBuilder.build(transcript);
        String response = ollamaChatModel.call(prompt);

        if(response == null || response.isBlank()) {
            throw new IllegalStateException("Respuesta Vacia del modelo", null);
        }

        try {
            return objectMapper.readValue(response, MeetingAnalysisResponse.class);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Error al parsear JSON de la respuesta", e);
        }
    }
}
  • @Service: Indica que esta clase es un servicio Spring, es decir, una clase de lógica de negocio que puede ser inyectada en otros componentes.
  • @AllArgsConstructor: De Lombok, genera automáticamente un constructor con todos los campos como parámetros. Esto facilita la inyección de dependencias por constructor.
  • OllamaChatModel ollamaChatModel: Nos sirve para conectarnos al modelo de lenguaje de Ollama
  • ObjectMapper: De Jackson, se usa para convertir (parsear) texto JSON en objetos Java. Aquí convierte la respuesta JSON del modelo en un MeetingAnalysisResponse.

AudioExtractorService

La clase AudioExtractorService es un servicio de Spring que se encarga de extraer el audio de un archivo de video (por ejemplo, .mp4, .mov, etc.) usando la herramienta de línea de comandos FFmpeg. Este audio extraído se guarda como un archivo .wav.


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;

@Service
@Slf4j
public class AudioExtractorService {

    public File extractAudio(File videoFile) throws IOException, InterruptedException {
        File audioFile = File.createTempFile("audio", ".wav");
        audioFile.deleteOnExit();

        ProcessBuilder pb = new ProcessBuilder(
                "ffmpeg", "-y", "-i", videoFile.getAbsolutePath(),
                "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
                audioFile.getAbsolutePath()
        );
        pb.redirectErrorStream(true);
        Process process = pb.start();
        process.getOutputStream().close();

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                log.info("[ffmpeg] {}", line);
            }
        }

        int exitCode = process.waitFor();
        if (exitCode != 0) {
            throw new RuntimeException("Error al extraer audio con ffmpeg. Código de salida: " + exitCode);
        }

        log.info("Audio extraído correctamente: {}", audioFile.getAbsolutePath());
        log.info("Tamaño archivo audio generado: {} bytes", audioFile.length());
        return audioFile;
    }
}
  • @Slf4j: De Lombok, permite usar el logger log sin declararlo manualmente (log.info(), log.error(), etc.).

Esta linea para entenderla mejor:


ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-y", "-i", videoFile.getAbsolutePath(), ...);

Este comando le dice a ffmpeg que:

  • Tome el archivo de video de entrada.
  • Extraiga solo el audio (-vn).
  • Codifique el audio a:
    • PCM lineal sin compresión (pcm_s16le)
    • Frecuencia de muestreo de 16000 Hz (-ar 16000)
    • Mono canal (-ac 1)
  • Escriba el audio en el archivo temporal creado.

AudioTranscriptionService

La clase AudioTranscriptionService es un servicio de Spring que se encarga de transcribir audio a texto usando un script Python externo (basado en Whisper de OpenAI). Transcribe un archivo de audio (como un .wav) llamando a un script Python (transcribe.py) que debe estar ubicado en el directorio resources/scripts/ del proyecto.


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

@Service
@Slf4j
public class AudioTranscriptionService {

    public String transcribeAudio(File audioFile) throws IOException, InterruptedException {
        File scriptFile = File.createTempFile("transcribe", ".py");
        scriptFile.deleteOnExit();

        try (InputStream in = getClass().getClassLoader().getResourceAsStream("scripts/transcribe.py")) {
            if (in == null) {
                throw new FileNotFoundException("No se encontró transcribe.py en resources/scripts/");
            }
            Files.copy(in, scriptFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
        }

        log.info("Ejecutando script Python de transcripción: {}", scriptFile.getAbsolutePath());

        ProcessBuilder pb = new ProcessBuilder("/usr/local/bin/python3", scriptFile.getAbsolutePath(), audioFile.getAbsolutePath());
        pb.redirectErrorStream(true);
        Process process = pb.start();

        StringBuilder outputBuilder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                log.info("[Python] {}", line);
                outputBuilder.append(line).append("\n");
            }
        }

        int exitCode = process.waitFor();
        String output = outputBuilder.toString().trim();

        if (exitCode != 0) {
            throw new RuntimeException("Whisper falló al transcribir el audio. Código de salida: " + exitCode + "\nSalida del script:\n" + output);
        }

        log.info("Transcripción obtenida:\n{}", output);

        return output;
    }
}

Creando el script transcribe.py

¿Qué es Whisper?

Whisper es un modelo de inteligencia artificial de código abierto desarrollado por OpenAI para reconocimiento automático de voz (ASR, por sus siglas en inglés). Su función principal es transcribir audio a texto, y lo puede hacer en múltiples idiomas con alta precisión.

Este script de Python utiliza Whisper, para transcribir un archivo de audio a texto en español.


import whisper
import sys

print("Inicio de la transcripción...", flush=True)

model = whisper.load_model("base")
result = model.transcribe(sys.argv[1], language="es")

print("Transcripción completada.", flush=True)
print(result["text"], flush=True)

MeetingServiceFile

La clase MeetingServiceFile es un servicio de Spring que se encarga de procesar archivos de video (por ejemplo, grabaciones de reuniones) y transformarlos en un resumen estructurado en JSON. Es la puerta de entrada para procesar un MultipartFile (archivo subido por el usuario).


import com.amoelcodigo.meetinganalysis.dto.MeetingAnalysisResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Service
@Slf4j
@AllArgsConstructor
public class MeetingServiceFile {

    private final AudioExtractorService audioExtractorService;
    private final AudioTranscriptionService audioTranscriptionService;
    private final MeetingService meetingService;

    public MeetingAnalysisResponse processFile(MultipartFile file) throws IOException, InterruptedException {
        log.info("processFile iniciado con archivo: {}", file.getOriginalFilename());

        File videoFile = null;
        File audioFile = null;

        try {
            videoFile = File.createTempFile("video", ".mp4");
            videoFile.deleteOnExit();
            file.transferTo(videoFile.toPath());

            audioFile = audioExtractorService.extractAudio(videoFile);

            String transcript = audioTranscriptionService.transcribeAudio(audioFile);

            return meetingService.analyzeMeeting(transcript);
        } finally {
            deleteFileIfExists(videoFile);
            deleteFileIfExists(audioFile);
        }
    }

    private void deleteFileIfExists(File file) {
        if (file != null && file.exists()) {
            if (file.delete()) {
                log.info("Archivo temporal borrado: {}", file.getAbsolutePath());
            } else {
                log.warn("No se pudo borrar archivo temporal: {}", file.getAbsolutePath());
            }
        }
    }
}

Creando el Controller

La clase MeetingController es un controlador REST en Spring Boot. Se encarga de exponer dos endpoints HTTP POST para analizar reuniones. Es el punto de entrada para clientes como frontends, aplicaciones móviles o scripts externos.


import com.amoelcodigo.meetinganalysis.dto.MeetingAnalysisRequest;
import com.amoelcodigo.meetinganalysis.dto.MeetingAnalysisResponse;
import com.amoelcodigo.meetinganalysis.service.MeetingService;
import com.amoelcodigo.meetinganalysis.service.MeetingServiceFile;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import java.io.IOException;

@RestController
@RequestMapping("/api/meeting")
@AllArgsConstructor
public class MeetingController {

    private final MeetingService meetingService;
    private final MeetingServiceFile meetingServiceFile;

    @PostMapping("/analyze")
    public ResponseEntity<MeetingAnalysisResponse> analyze(@RequestBody @Valid final MeetingAnalysisRequest request) {
        return ResponseEntity.ok(this.meetingService.analyzeMeeting(request.transcript()));
    }

    @PostMapping("/upload")
    public ResponseEntity<MeetingAnalysisResponse> analyzeVideo(@RequestParam("file") final MultipartFile file) throws IOException, InterruptedException {
        return ResponseEntity.ok(this.meetingServiceFile.processFile(file));
    }

}
  • /api/meeting/analyze -> Transcribe y analiza un texto ya dado (no video).
  • /api/meeting/upload -> Recibe un archivo de video y procesa todo (audio → texto → análisis).

Configuración de Spring AI y Ollama

Antes de iniciar nuestro proyecto en el archivo application.properties debemos escribir unas configuraciones.

spring.application.name=meeting-analysis
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=llama3.2
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
logging.level.com.amoelcodigo.meetinganalysis=INFO
  • spring.application.name=meeting-analysis → Define el nombre de la aplicación para identificación en logs y configuraciones.

  • spring.ai.ollama.base-url=http://localhost:11434 → URL base personalizada para conectar con el servicio AI Ollama.

  • spring.ai.ollama.chat.model=llama3.2 → Modelo AI personalizado que se usará para las consultas al servicio Ollama.

  • spring.servlet.multipart.max-file-size=100MB → Tamaño máximo permitido por archivo para subir (multipart).

  • spring.servlet.multipart.max-request-size=100MB → Tamaño máximo permitido para toda la solicitud multipart.

  • logging.level.com.amoelcodigo.meetinganalysis=INFO → Nivel de logging para el paquete indicado (INFO, WARN, ERROR se mostrarán).


Ejectutar el proyecto

  1. Asegúrate que Ollama este corriendo.
  2. Corre tu aplicación Spring Boot desde el IDE o por consola
  3. Envía una petición con Postman o curl:
curl -X POST http://localhost:8080/api/meeting/analyze \
  -H "Content-Type: application/json" \
  -d '{"transcript": "Juan: Necesitamos lanzar el producto el lunes. María: Sí, pero falta probar el módulo de pagos."}'
  1. Para probar el endpoint del video recomiendo bajar un video de youtube con cualquier herramienta, dejo el siguiente link a un ejemplo Ejemplo de video de reunión para pruebas

  2. Luego por medio de postman nos ubicamos en body y mandamos una key de nombre file y tipo File Ejemplo de envío de video por Postman

Video Tutorial

🔗 Mira el video tutorial aquí


Repositorio con el Ejemplo

Puedes encontrar el código completo de este ejemplo en el siguiente repositorio de GitHub:

🔗 Repositorio en GitHub: MeetingAnalysis


git clone https://github.com/VManuelPM/meetingAnalysis.git
cd meetingAnalysis

Conclusión

Con Meeting Analysis automatizamos un proceso complejo de transformar reuniones largas en resúmenes claros y accionables. Combinando Spring Boot con modelos de lenguaje como Ollama y herramientas de procesamiento de audio como ffmpeg y Whisper, podemos construir soluciones inteligentes que ahorran tiempo y mejoran la productividad en el trabajo diario.