画像の部分が終わったら、音声の処理に入ります。音声には 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 種類の返却があります。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 つはありません。
これはかなり影響があります。なぜなら、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>