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-1 | tts-1 |
spring.ai.openai.audio.speech.options.voice | 输出的语音,比如男音/女音。可用选项包括:alloy, echo, fable, onyx, nova, shimmer | alloy |
spring.ai.openai.audio.speech.options.response-format | 音频输出的格式。支持的格式包括 mp3、opus、aac、flac、wav 和 pcm | mp3 |
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测试
将请求返回的二进制数组,通过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已分析,设计OpenAiAudioTranscriptionModel
和 OpenAiAudioSpeechModel
共用 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.prompt | An 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限额原因,暂时无法测试,大家可运行代码自行测试下效果。
源码示例
总结
本篇文章根据官方文章对配置参数进行简单的说明,并提供了简单的实现示例。并未对tts、whisper模型的实现原理进行说明,待个人对这部分知识补齐之后在做补充。