Artículo

Una Guía para la Conversión de Texto a Voz (TTS) de OpenAI en Spring AI

Una Guía para la Conversión de Texto a Voz (TTS) de OpenAI en Spring AI

1. Introducción

Hoy en día, las aplicaciones se benefician enormemente de la integración de redes neuronales, como bases de conocimiento, asistentes o motores de análisis. Un caso de uso práctico es convertir texto en voz. Este proceso, conocido como Text-to-Speech (TTS), permite la creación automatizada de contenido de audio con voces naturales y semejantes a las humanas.

Los sistemas modernos de TTS utilizan aprendizaje profundo para manejar pronunciación, ritmo, entonación e incluso emoción. A diferencia de los métodos basados en reglas tempranos, estos modelos se entrenan con grandes conjuntos de datos y pueden generar voz expresiva y multilingüe, lo que es ideal para aplicaciones globales como asistentes virtuales o plataformas de educación inclusiva.

En este tutorial, exploraremos cómo usar OpenAI Text-to-Speech con Spring AI.

2. Dependencias y configuración

Comenzaremos agregando la spring-ai-starter-model-openai dependencia:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.1.0</version>
</dependency>

Luego, configuraremos las propiedades de Spring AI para el modelo OpenAI:

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.audio.speech.options.model=tts-1
spring.ai.openai.audio.speech.options.voice=alloy
spring.ai.openai.audio.speech.options.response-format=mp3
spring.ai.openai.audio.speech.options.speed=1.0

Para usar la API de OpenAI, debemos establecer la clave de API de OpenAI. También tenemos que especificar el nombre del modelo de texto a voz, la voz, el formato de respuesta y la velocidad de audio.

3. Construir la aplicación de texto a voz

Ahora construiremos nuestra aplicación de texto a voz. En primer lugar, crearemos el TextToSpeechService: (código)

@Service
public class TextToSpeechService {

    private OpenAiAudioSpeechModel openAiAudioSpeechModel;

    @Autowired
    public TextToSpeechService(OpenAiAudioSpeechModel openAiAudioSpeechModel) {
        this.openAiAudioSpeechModel = openAiAudioSpeechModel;
    }

    public byte[] makeSpeech(String text) {

        SpeechPrompt speechPrompt = new SpeechPrompt(text);

        SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
        return response.getResult().getOutput();
    }
}

Aquí usamos OpenAiAudioSpeechModel, que Spring AI preconfigura con nuestras propiedades. También definimos el método makeSpeech() para convertir texto en bytes de archivo de audio con OpenAiAudioSpeechModel.

A continuación, creamos el TextToSpeechController: (código)

@RestController
public class TextToSpeechController {
    private final TextToSpeechService textToSpeechService;

    @Autowired
    public TextToSpeechController(TextToSpeechService textToSpeechService) {
        this.textToSpeechService = textToSpeechService;
    }

    @GetMapping("/text-to-speech")
    public ResponseEntity<byte[]> generateSpeechForText(@RequestParam String text) {
        return ResponseEntity.ok(textToSpeechService.makeSpeech(text));
    }
}

Finalmente, probamos nuestro punto final: (código)

@SpringBootTest
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
class TextToSpeechLiveTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private TextToSpeechService textToSpeechService;

    @Test
    void givenTextToSpeechService_whenCallingTextToSpeechEndpoint_thenExpectedAudioFileBytesShouldBeObtained() throws Exception {
        byte[] audioContent = mockMvc.perform(get("/text-to-speech")
          .param("text", "Hello from CodeJa"))
          .andExpect(status().isOk())
          .andReturn()
          .getResponse()
          .getContentAsByteArray();

        assertNotEquals(0, audioContent.length);
    }
}

Llamamos al punto final de texto a voz y verificamos el código de respuesta y el contenido no vacío. Si guardamos el contenido en un archivo, obtenemos un archivo MP3 con nuestra voz.

4. Añadir un punto final de audio en tiempo real con streaming

Podemos enfrentar un consumo significativo de memoria al obtener contenido de audio grande en una única matriz de bytes enorme. Además, a veces deseamos reproducir el audio antes de que se cargue completamente. Para esto, OpenAI admite respuestas de texto a voz en streaming.

Vamos a ampliar nuestro TextToSpeechService para soportar esta funcionalidad: (código)

public Flux<byte[]> makeSpeechStream(String text) {
    SpeechPrompt speechPrompt = new SpeechPrompt(text);
    Flux<SpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);

    return responseStream
      .map(SpeechResponse::getResult)
      .map(Speech::getOutput);
}

Hemos añadido el método makeSpeechStream(). Aquí usamos el método stream() de OpenAiAudioSpeechModel para producir un flujo de fragmentos de bytes.

A continuación, creamos el punto final para transmitir bytes por HTTP: (código)

@GetMapping(value = "/text-to-speech-stream", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<StreamingResponseBody> streamSpeech(@RequestParam("text") String text) {
    Flux<byte[]> audioStream = textToSpeechService.makeSpeechStream(text);

    StreamingResponseBody responseBody = outputStream -> {
        audioStream.toStream().forEach(bytes -> {
            try {
                outputStream.write(bytes);
                outputStream.flush();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    };

    return ResponseEntity.ok()
      .contentType(MediaType.APPLICATION_OCTET_STREAM)
      .body(responseBody);
}

Aquí iteramos sobre el flujo de bytes y lo escribimos en el StreamingResponseBody. Si usamos WebFlux, devolveríamos el Flux directamente desde el punto final. Como una opción, podemos usar el tipo de contenido application/octet-stream para indicar que la respuesta es un flujo.

Ahora probemos nuestro método de streaming: (código)

@Test
void givenStreamingEndpoint_whenCalled_thenReceiveAudioFileBytes() throws Exception {

    String longText = """
          Hello from CodeJa!
          Here, we explore the world of Java,
          Spring, and web development with clear, practical tutorials.
          Whether you're just starting out or diving deep into advanced
          topics, you'll find guides to help you write clean, efficient,
          and modern code.
          """;

    mockMvc.perform(get("/text-to-speech-stream")
        .param("text", longText)
        .accept(MediaType.APPLICATION_OCTET_STREAM))
      .andExpect(status().isOk())
      .andDo(result -> {
          byte[] response = result.getResponse().getContentAsByteArray();
          assertNotNull(response);
          assertTrue( response.length > 0);
      });
}

Llamamos a nuestro punto final de streaming y verificamos que devuelve un array de bytes. MockMvc recoge el cuerpo completo de la respuesta, pero también podemos leerlo como un flujo.

5. Personalizar los parámetros del modelo para una llamada específica

A veces necesitamos anular las opciones del modelo para llamadas específicas. Para esto, podemos usar OpenAiAudioSpeechOptions. Actualicemos nuestro TextToSpeechService para soportar opciones de voz personalizadas: (código)

public byte[] makeSpeech(String text, OpenAiAudioSpeechOptions speechOptions) {
    SpeechPrompt speechPrompt = new SpeechPrompt(text, speechOptions);

    SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

    return response.getResult().getOutput();
}

Hemos anulado makeSpeech() y añadido el parámetro OpenAiAudioSpeechOptions. Lo usamos como parámetro de la llamada a la API de OpenAI. Si pasamos un objeto vacío, se aplicarán las opciones por defecto.

A continuación, creamos otro punto final que acepta parámetros de voz: (código)

@GetMapping("/text-to-speech-customized")
public ResponseEntity<byte[]> generateSpeechForTextCustomized(@RequestParam("text") String text, @RequestParam Map<String, String> params) {
    OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
      .model(params.get("model"))
      .voice(OpenAiAudioApi.SpeechRequest.Voice.valueOf(params.get("voice")))
      .responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.valueOf(params.get("responseFormat")))
      .speed(Float.parseFloat(params.get("speed")))
      .build();

    return ResponseEntity.ok(textToSpeechService.makeSpeech(text, speechOptions));
}

Aquí obtenemos un mapa de parámetros de voz y construimos el OpenAiAudioSpeechOptions.

Finalmente, probemos el nuevo punto final: (código)

@Test
void givenTextToSpeechService_whenCallingTextToSpeechEndpointWithAnotherVoiceOption_thenExpectedAudioFileBytesShouldBeObtained() throws Exception {
    byte[] audioContent = mockMvc.perform(get("/text-to-speech-customized")
      .param("text", "Hello from CodeJa")
      .param("model", "tts-1")
      .param("voice", "NOVA")
      .param("responseFormat", "MP3")
      .param("speed", "1.0"))
    .andExpect(status().isOk())
    .andReturn()
    .getResponse()
    .getContentAsByteArray();

    assertNotEquals(0, audioContent.length);
}

Llamamos al punto final y usamos la voz NOVA para esta solicitud. Como se esperaba, recibimos los bytes de audio con la voz anula.

6. Conclusión

Las APIs de texto a voz hacen posible generar voz natural a partir del texto. Con una configuración sencilla y modelos modernos, podemos llevar una interacción hablada y dinámica a nuestras aplicaciones.

En este artículo, exploramos cómo integrar nuestra aplicación con el modelo TTS de OpenAI utilizando Spring AI. De la misma manera, podemos integrar con otros modelos TTS o construir el nuestro.

Newsletter Semanal de Java

Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.

Sin spam. Cancela cuando quieras.

Compartir artículo