Spring AI 接入OpenAI实现文字转语音、语音转文字功能

1,013 阅读6分钟

AI 大模型在文本、图片等领域非常强悍,人工智能在音频生成等领域也能大展拳脚。本篇文章主要介绍Spring AI 接入 OpenAI TTS大模型实现文字转语音和Whisper大模型实现语音转换文字功能。

由于本人对于TTS、Whisper大模型理解不够深入,这里就不对其实现原理进行讲解,只对Spring AI的使用进行演示。

OpenAI tts(Text-to-Speech) 模型

Text-to-Speech(TTS,文本生成语音,或者称之为语音合成)。

它可以做哪些事?

  • 可以帮助你完成一篇文章的阅读。
  • 可以帮助你制作多种语言的视频。
  • 可以帮助你使用流媒体提供实时音频输出。

Spring AI 源码分析

Spring AI 提供对 OpenAI 语音 API 的支持。并抽取公共 SpeechModel 接口和 StreamingSpeechModel 接口,可以实现后续其它tts大模型的快速扩展和集成。

OpenAiAudioSpeechModel 类结构

classDiagram

Model<|--SpeechModel
StreamingModel <|-- StreamingSpeechModel
SpeechModel <|.. OpenAiAudioSpeechModel
StreamingSpeechModel <|.. OpenAiAudioSpeechModel
OpenAiAudioSpeechModel ..> OpenAiAudioApi
OpenAiAudioSpeechModel ..> OpenAiAudioSpeechOptions

class SpeechModel {
    default byte[] call(String message)
    + SpeechResponse call(SpeechPrompt request)
}
class StreamingSpeechModel {
    default Flux<byte[]> stream(String message)
    +Flux<SpeechResponse> stream(SpeechPrompt request)
}
class OpenAiAudioApi {
    - restClient
    - webClient
}
class OpenAiAudioSpeechOptions {
    - model
    - input
    - voice
    - response_format
    - speed
}

OpenAiAudioApi源码

public class OpenAiAudioApi {
    // RestClient 普通的Http调用客户端,同步调用
    private final RestClient restClient;
    // webClient 响应式编程调用客户端,流式调用
    private final WebClient webClient;

    // ------------------------------ 构造方法 -------------------------------------
    public OpenAiAudioApi(String openAiToken) {
        this("https://api.openai.com", openAiToken, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
    }

    public OpenAiAudioApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
        this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders((headers) -> {
            headers.setBearerAuth(openAiToken);
        }).defaultStatusHandler(responseErrorHandler).build();
        this.webClient = WebClient.builder().baseUrl(baseUrl).defaultHeaders((headers) -> {
            headers.setBearerAuth(openAiToken);
        }).defaultHeaders(ApiUtils.getJsonContentHeaders(openAiToken)).build();
    }

    public OpenAiAudioApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) {
        this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders((headers) -> {
            headers.setBearerAuth(openAiToken);
        }).defaultStatusHandler(responseErrorHandler).build();
        this.webClient = webClientBuilder.baseUrl(baseUrl).defaultHeaders((headers) -> {
            headers.setBearerAuth(openAiToken);
        }).defaultHeaders(ApiUtils.getJsonContentHeaders(openAiToken)).build();
    }
   // ------------------------------ 构造方法 -------------------------------------

   // 调用api,将文本转为音频,同步方式
    public ResponseEntity<byte[]> createSpeech(SpeechRequest requestBody) {
        return ((RestClient.RequestBodySpec)this.restClient.post().uri("/v1/audio/speech", new Object[0])).body(requestBody).retrieve().toEntity(byte[].class);
    }
   // 调用api,将文本转为音频, 流式方式
    public Flux<ResponseEntity<byte[]>> stream(SpeechRequest requestBody) {
        return ((WebClient.RequestBodySpec)this.webClient.post().uri("/v1/audio/speech", new Object[0])).body(Mono.just(requestBody), SpeechRequest.class).accept(new MediaType[]{MediaType.APPLICATION_OCTET_STREAM}).exchangeToFlux((clientResponse) -> {
            HttpHeaders headers = clientResponse.headers().asHttpHeaders();
            return clientResponse.bodyToFlux(byte[].class).map((bytes) -> {
                return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).body(bytes);
            });
        });
    }
   // 调用api,将语音转文本
    public ResponseEntity<?> createTranscription(TranscriptionRequest requestBody) {
        return this.createTranscription(requestBody, requestBody.responseFormat().getResponseType());
    }
    // 调用api,将语音转文本
    public <T> ResponseEntity<T> createTranscription(TranscriptionRequest requestBody, Class<T> responseType) {
        MultiValueMap<String, Object> multipartBody = new LinkedMultiValueMap();
        multipartBody.add("file", new ByteArrayResource(requestBody.file()) {
            public String getFilename() {
                return "audio.webm";
            }
        });
        multipartBody.add("model", requestBody.model());
        multipartBody.add("language", requestBody.language());
        multipartBody.add("prompt", requestBody.prompt());
        multipartBody.add("response_format", requestBody.responseFormat().getValue());
        multipartBody.add("temperature", requestBody.temperature());
        if (requestBody.granularityType() != null) {
            Assert.isTrue(requestBody.responseFormat() == OpenAiAudioApi.TranscriptResponseFormat.VERBOSE_JSON, "response_format must be set to verbose_json to use timestamp granularities.");
            multipartBody.add("timestamp_granularities[]", requestBody.granularityType().getValue());
        }

        return ((RestClient.RequestBodySpec)this.restClient.post().uri("/v1/audio/transcriptions", new Object[0])).body(multipartBody).retrieve().toEntity(responseType);
    }

    public ResponseEntity<?> createTranslation(TranslationRequest requestBody) {
        return this.createTranslation(requestBody, requestBody.responseFormat().getResponseType());
    }
    // // 调用api,将语音翻译
    public <T> ResponseEntity<T> createTranslation(TranslationRequest requestBody, Class<T> responseType) {
        MultiValueMap<String, Object> multipartBody = new LinkedMultiValueMap();
        multipartBody.add("file", new ByteArrayResource(requestBody.file()) {
            public String getFilename() {
                return "audio.webm";
            }
        });
        multipartBody.add("model", requestBody.model());
        multipartBody.add("prompt", requestBody.prompt());
        multipartBody.add("response_format", requestBody.responseFormat().getValue());
        multipartBody.add("temperature", requestBody.temperature());
        return ((RestClient.RequestBodySpec)this.restClient.post().uri("/v1/audio/translations", new Object[0])).body(multipartBody).retrieve().toEntity(responseType);
    }
}

OpenAiAudioApi实现了语音转文字、文字转语音、翻译功能,由于代码太长,只保留了方法。在OpenAiAudioApi中还有有请求参数、返回参数的定义,模型的控制参数定义等等。如果在使用时,不知道用哪个值,可以在该类中查找。

tts模型属性

参数与OpenAiAudioSpeechOptions类对应。为更好的使用我们先来了解下控制大模型的一些参数;

属性名作用默认值
spring.ai.openai.audio.speech.options.model要使用的模型的 ID。目前只有 tts-1tts-1
spring.ai.openai.audio.speech.options.voice输出的语音,比如男音/女音。可用选项包括:alloy, echo, fable, onyx, nova, shimmeralloy
spring.ai.openai.audio.speech.options.response-format音频输出的格式。支持的格式包括 mp3、opus、aac、flac、wav 和 pcmmp3
spring.ai.openai.audio.speech.options.speed语音合成的速度。范围从 0.0(最慢)到 1.0(最快)1.0

示例(文本转语音)

package org.ivy.controller;

import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.audio.speech.SpeechPrompt;
import org.springframework.ai.openai.audio.speech.SpeechResponse;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;


@RestController
public class AudioController {

    private final OpenAiAudioSpeechModel openAiAudioSpeechModel;
    public AudioController(OpenAiAudioSpeechModel openAiAudioSpeechModel) {
        this.openAiAudioSpeechModel = openAiAudioSpeechModel;
    }
    // 同步方式文本生成语音
    @GetMapping(value = "tts", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public byte[] speech(@RequestParam(defaultValue = "Hello, this is a text-to-speech example.") String text) {
        OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
                .withModel("tts-1") // 指定模型, 目前Spring AI支持一种tts-1,可以不配置
                .withVoice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY) // 指定生成的音色
                .withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3) // 指定生成音频的格式
                .withSpeed(1.0f) // 指定生成速度
                .build();
        SpeechPrompt speechPrompt = new SpeechPrompt(text, speechOptions);
        SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
        return response.getResult().getOutput(); // 返回语音byte数组
    }

    // 流式方式文本生成语音
    @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<byte[]> stream(@RequestParam(defaultValue = "Today is a wonderful day to build something people love!") String text) {
        OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
                .withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
                .build();
        SpeechPrompt speechPrompt = new SpeechPrompt(text, speechOptions);
        Flux<SpeechResponse> stream = openAiAudioSpeechModel.stream(speechPrompt);
        return stream.map(speechResponse -> speechResponse.getResult().getOutput()).flatMapSequential(Flux::just);
    }
}

实现了同步和流式实现文本转语音功能

Postman测试

image.png

将请求返回的二进制数组,通过Postman保存为本地的mp3文件即可听到效果。另外大家自己可以做一个页面,将返回的二进制音频通过播放器的库进行播放,就可以实现一个简单的文本播报功能的网站了,我将打算在我的示例代码中实现,提交到Github上,大家可以参考学习。

Whisper 模型

Whisper 是一种通用的语音识别模型。它是在包含各种音频的大型数据集上训练的,也是一个多任务模型,可以执行多语言语音识别、语音翻译和语言识别。

Whisper 基于编码器-解码器(encoder-decoder)结构的Transformer模型,也称为序列到序列模型。这是一种在自然语言处理和语音识别领域广泛使用的深度学习架构。

关于Whisper模型的介绍,介绍模型的架构与训练模型的细节

它可以做什么?

  • 语音转文本,支持99种语言,无论用哪种语言录制的,都能转换为文本。比如:生成视频字幕
  • 语音识别。比如:语音助手功能
  • 语音翻译。比如:识别的文本从原始语言翻译为其它语言,跨语言交流工具

关于Whisper大模型的其它细节,训练数据、模型架构等等大家自行研究,本文还是主要分析 Spring AI 框架支持接入Whisper模型的源码分析以及实践。

Spring AI 实现源码

Spring AI 为 OpenAI 的转录 API 提供支持。当实现其他转录提供程序时,将提取一个通用 AudioTranscriptionModel 接口。

OpenAiAudioTranscriptionModel 类结构

classDiagram
Model <|.. OpenAiAudioTranscriptionModel
OpenAiAudioTranscriptionModel ..> OpenAiAudioApi
OpenAiAudioTranscriptionModel ..> OpenAiAudioTranscriptionOptions
OpenAiAudioTranscriptionModel
class Model {
    TRes call(TReq request);
}
class OpenAiAudioTranscriptionModel {
    - OpenAiAudioTranscriptionOptions defaultOptions
    - OpenAiAudioApi audioApi
    + AudioTranscriptionResponse call(AudioTranscriptionPrompt request)
}

OpenAiAudioTranscriptionModel主要负责请求/返回的封装、处理、调用、重试。而audioApi真正调用OpenAI。具体源码在tts已分析,设计OpenAiAudioTranscriptionModelOpenAiAudioSpeechModel 共用 OpenAiAudioApi

Whisper 模型属性

属性作用默认值
spring.ai.openai.audio.transcription.options.model要使用的模型的 ID。目前只有 whisper-1(由我们的开源 Whisper V2 模型提供支持)可用。whisper-1
spring.ai.openai.audio.transcription.options.response-format输出的格式,位于以下选项之一中:json、text、srt、verbose_json 或 vtt。json
spring.ai.openai.audio.transcription.options.promptAn optional text to guide the model’s style or continue a previous audio segment. The prompt should match the audio language. 用于指导模型样式或继续上一个音频片段的可选文本。提示应与音频语言匹配
spring.ai.openai.audio.transcription.options.language输入音频的语言。以 ISO-639-1 格式提供输入语言将提高准确性和延迟
spring.ai.openai.audio.transcription.options.temperature采样温度,介于 0 和 1 之间。较高的值(如 0.8)将使输出更具随机性,而较低的值(如 0.2)将使其更加集中和确定。如果设置为 0,模型将使用对数概率自动提高温度,直到达到某些阈值0
spring.ai.openai.audio.transcription.options.timestamp_granularities要为此听录填充的时间戳粒度。必须verbose_json设置response_format才能使用时间戳粒度。支持以下任一或两个选项:word 或 segment。注意:分段时间戳没有额外的延迟,但生成字时间戳会产生额外的延迟segment

示例 (语音转文本)

package org.ivy.controller;

import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
import org.springframework.ai.openai.OpenAiAudioTranscriptionOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.openai.audio.transcription.AudioTranscriptionResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TranscriptionController {

    @Value("classpath:audio.mp3")
    private org.springframework.core.io.Resource audioResource;

    private final OpenAiAudioTranscriptionModel openAiTranscriptionModel;

    public TranscriptionController(OpenAiAudioTranscriptionModel openAiTranscriptionModel) {
        this.openAiTranscriptionModel = openAiTranscriptionModel;
    }

    @GetMapping("audio2Text")
    public String audio2Text() {
        var transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
                .withResponseFormat(OpenAiAudioApi.TranscriptResponseFormat.TEXT)
                .withTemperature(0f)
                .build();
        AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioResource, transcriptionOptions);
        AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptionRequest);
        return response.getResult().getOutput();
    }

}

将上一小节生成的mp3文件进行反向转换为文本。 由于token限额原因,暂时无法测试,大家可运行代码自行测试下效果。

源码示例

github.com/fangjieDevp…

总结

本篇文章根据官方文章对配置参数进行简单的说明,并提供了简单的实现示例。并未对tts、whisper模型的实现原理进行说明,待个人对这部分知识补齐之后在做补充。