spring AI (八) 语音转录与TTS

搞完图片的部分就该搞语音了，语音这里面有 2 个方法：

Transcription API 用来转录文本，就是把语音生成字幕，使用的是 whisper 模型

Text-To-Speech (TTS) API 简称 TTS，就是使用文本生成语音

Transcription

直接放代码把，都是一样的操作

Copy
private final OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel;

/**
* 语音转录
* @param file 语音文件
* @return String
*/
@PostMapping(value = "/transcriptions")
public String transcriptions(@RequestPart("file") MultipartFile file) {

var transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
.withResponseFormat(OpenAiAudioApi.TranscriptResponseFormat.TEXT)
.withTemperature(0f)
.build();

AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(file.getResource(), transcriptionOptions);
AudioTranscriptionResponse response = openAiAudioTranscriptionModel.call(transcriptionRequest);
return response.getResult().getOutput();
}

里面最重要的参数就是 ResponseFormat 生成的格式，可选 txt , json, srt 等。srt 就是通用的字幕文件格式了。其余的参数配置可看官方文档

唯一需要注意的是，需要的文件格式是 Resource

如果使用的是中转 API，测试前请查看是否支持此模型

TTS

TTS 有 2 种返回，一种普通的，还有一种是流式返回。普通的就不说了，只讲流式的。代码如下:

Copy
private final OpenAiAudioSpeechModel openAiAudioSpeechModel;

/**
* TTS实时流
* @param message 文本
* @return SseEmitter
*/
@GetMapping(value = "/tts", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter openImage(@RequestParam String message) {
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.withVoice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
.withSpeed(1.0f)
.withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.withModel(OpenAiAudioApi.TtsModel.TTS_1_HD.value)
.build();

SpeechPrompt speechPrompt = new SpeechPrompt(message, speechOptions);

String uuid = UUID.randomUUID().toString();
SseEmitter emitter = SseEmitterUtils.connect(uuid);

Flux<SpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);
responseStream.subscribe(response -> {
byte[] output = response.getResult().getOutput();
String base64Audio = Base64.getEncoder().encodeToString(output);
SseEmitterUtils.sendMessage(uuid, base64Audio);
});
return emitter;
}

各个参数的解释如下：

参数 | 解释
Voice | 讲述人语音
Speed | 语音合成的速度。可接受的范围是从 0.0（最慢）到 1.0（最快）
ResponseFormat | 音频输出的格式，支持的格式有 mp3、opus、aac、flac、wav 和 pcm。
Model | 模型，有 TTS_1 和 TTS_1_HD，HD 生成的效果更好

需要注意的是，音频输出的格式目前只有前 4 个，后 2 个是没有的

这个影响还是挺大的，因为 PCM 是可以在浏览器直接解码的，适合流，MP3 还需要转码。

可以看到，TTS 的生成结果是一个 byte[] 数组，返回时转成了 Base64，最后通过 SSE 发送到前端。

前端还需要解码，我直接让 Claude 写了个测试页面：

Copy




实时流式 MP3 TTS 播放器


实时流式 MP3 TTS 播放器

开始播放



let mediaSource;
let sourceBuffer;
let audioQueue = [];
let isPlaying = false;

function startStreaming() {
const text = document.getElementById('textInput').value;
const encodedText = encodeURIComponent(text);
const eventSource = new EventSource(`http://127.0.0.1:8868/audio/tts?message=${encodedText}`);

const audio = document.getElementById('audioPlayer');
mediaSource = new MediaSource();
audio.src = URL.createObjectURL(mediaSource);

mediaSource.addEventListener('sourceopen', function() {
sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
sourceBuffer.addEventListener('updateend', playNextChunk);
});

audio.play();

eventSource.onopen = function(event) {
console.log('Connection opened');
};

eventSource.onmessage = function(event) {
const audioChunk = base64ToArrayBuffer(event.data);
audioQueue.push(audioChunk);
if (!isPlaying) {
playNextChunk();
}
};

eventSource.onerror = function(error) {
console.error('Error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
eventSource.close();
};
}

function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

function playNextChunk() {
if (audioQueue.length > 0 && !sourceBuffer.updating) {
isPlaying = true;
const chunk = audioQueue.shift();
sourceBuffer.appendBuffer(chunk);
} else {
isPlaying = false;
}
}


