qinfengge

qinfengge

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

春の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 で、生成されるフォーマットです。選択肢は txtjsonsrt などです。srt は一般的な字幕ファイルフォーマットです。他のパラメータの設定については公式文書を参照してください。

唯一注意が必要なのは、必要なファイルフォーマットが Resource であることです。

中継 API を使用する場合は、テスト前にこのモデルがサポートされているか確認してください。

TTS#

TTS には 2 種類の返却があります。1 つは通常のもので、もう 1 つはストリーミング返却です。通常のものについては触れず、ストリーミングのものだけを説明します。コードは以下の通りです:

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('接続が開かれました');
        };

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

        eventSource.onerror = function(error) {
            console.error('エラー:', error);
            if (eventSource.readyState === EventSource.CLOSED) {
                console.log('接続が閉じられました');
            }
            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>
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。