qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github
email
telegram

spring AI (八) 语音转录与TTS

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

  1. Transcription API 用来转录文本,就是把语音生成字幕,使用的是 whisper 模型
  2. 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 个是没有的

image

这个影响还是挺大的,因为 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>
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。