搞完图片的部分就该搞语音了,语音这里面有 2 个方法:
Transcription API
用来转录文本,就是把语音生成字幕,使用的是whisper
模型Text-To-Speech (TTS) API
简称 TTS,就是使用文本生成语音
Transcription#
直接放代码把,都是一样的操作
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 种返回,一种普通的,还有一种是流式返回。普通的就不说了,只讲流式的。代码如下:
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 写了个测试页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>实时流式 MP3 TTS 播放器</title>
</head>
<body>
<h1>实时流式 MP3 TTS 播放器</h1>
<input type="text" id="textInput" placeholder="输入要转换的文本">
<button onclick="startStreaming()">开始播放</button>
<audio id="audioPlayer" controls></audio>
<script>
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;
}
}
</script>
</body>
</html>