Spring AI 中的 OpenAI 文本转语音(TTS)指南

1、概览

如今,应用程序极大地受益于神经网络集成,例如知识库、助手或分析引擎。其中一个实际用例是将文本转换为语音。这一过程被称为 文本转语音(Text-to-Speech,TTS),能够以自然逼真、类人的声音自动生成音频内容。

现代 TTS 系统利用深度学习技术处理发音、节奏、语调甚至情感。与早期的基于规则的方法不同,这些模型通过海量数据集训练,可以生成富有表现力的多语言语音,非常适合虚拟助手或包容性教育平台等全球化应用场景。

本文将带你了解如何在 Spring AI 中使用 OpenAI 文本转语音(TTS)。

2、依赖配置

首先,需要添加 spring-ai-starter-model-openai 依赖:

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

接下来,为 OpenAI 模型配置 Spring AI 属性:

# Api Key
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

要使用 OpenAI API,必须设置 OpenAI API Key。同时还需要指定 文本转语音(TTS) 的模型 名称、语音风格、响应格式以及 音频 速度。

3、构建文本转语音应用

现在,构建文本转语音应用。

首先,创建 TextToSpeechService Service 类:

@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();
    }
}

如上,我们通过构造函数注入 OpenAiAudioSpeechModel,它已通过 Spring AI 根据我们的配置属性完成预配置。

同时定义了 makeSpeech() 方法,用于通过 OpenAiAudioSpeechModel 将文本转换为音频文件字节。

接下来创建 TextToSpeechController Controller:

@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));
    }
}

最后,测试端点:

@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 Baeldung"))
          .andExpect(status().isOk())
          .andReturn()
          .getResponse()
          .getContentAsByteArray();

        assertNotEquals(0, audioContent.length);
    }
}

我们调用文本转语音端点,并验证响应状态码和非空内容。若将返回内容保存为文件,即可获得包含语音的 MP3 文件。

4、添加流式实时音频端点

当获取大容量音频内容时,单次返回巨大字节数组可能导致显著的内存消耗,甚至是内存溢出。此外,有时我们需要在音频完全响应前开始播放(流式播放)。为此,OpenAI 支持流式文本转语音响应。

扩展 TextToSpeechService 以支持该功能:

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);
}

我们新增了 makeSpeechStream() 方法,通过调用 OpenAiAudioSpeechModelstream() 方法来生成字节数据块流。

接下来创建通过 HTTP 流式传输字节的端点:

@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);
}

如上,我们遍历字节流并将其写入 StreamingResponseBody。若使用 WebFlux,则可直接从端点返回 Flux。另外,可指定 Content Typeapplication/octet-stream 来标识响应为数据流(可选)。

现在测试我们的流式处理方法:

@Test
void givenStreamingEndpoint_whenCalled_thenReceiveAudioFileBytes() throws Exception {

    String longText = """
          Hello from Baeldung!
          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);
      });
}

如上,我们调用流式端点并验证其返回的字节数组。MockMvc 会收集完整的响应体,但也可以按流式方式读取。

5、自定义模型参数

有时我们需要覆盖默认的模型选项。为此,可以使用 OpenAiAudioSpeechOptions

更新 TextToSpeechService 以支持自定义语音选项:

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

    SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);

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

如上,我们重写了 makeSpeech() 方法并添加 OpenAiAudioSpeechOptions 参数。调用 OpenAI API 时将其作为参数传入。若传递空对象,则应用默认选项。

现在创建另一个接收语音参数的端点:

@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));
}

如上,我们获取语音参数 Map 并构建 OpenAiAudioSpeechOptions

最后测试新端点:

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

    assertNotEquals(0, audioContent.length);
}

如上,调用端点并在本次请求中使用 NOVA 语音。如预期所示,返回的音频字节已应用覆盖后的语音参数。

6、总结

文本转语音(TTS)API 能够将文本转换为自然语音。通过简单配置和现代模型,我们可以为应用实现动态的语音交互功能。

本文介绍了如何利用 Spring AI 将应用与 OpenAI TTS 模型集成。采用相同方式,我们也能集成其他 TTS 模型或构建自定义解决方案。


Ref:https://www.baeldung.com/spring-ai-openai-tts