このバージョンはまだ開発中であり、まだ安定しているとは見なされていません。最新の安定バージョンについては、Spring AI 2.0.1 を使用してください! |
テキスト読み上げ (TTS) API
Spring AI は、TextToSpeechModel および StreamingTextToSpeechModel インターフェースを介して、テキスト読み上げ(TTS)のための統一 API を提供します。これにより、異なる TTS プロバイダー間で動作する移植性の高いコードを作成できます。
共通インターフェース
すべての TTS プロバイダーは、次の共有インターフェースを実装します。
TextToSpeechModel
TextToSpeechModel インターフェースは、テキストを音声に変換するためのメソッドを提供します。
public interface TextToSpeechModel extends Model<TextToSpeechPrompt, TextToSpeechResponse>, StreamingTextToSpeechModel {
/**
* Converts text to speech with default options.
*/
default byte[] call(String text) {
// Default implementation
}
/**
* Converts text to speech with custom options.
*/
TextToSpeechResponse call(TextToSpeechPrompt prompt);
/**
* Returns the default options for this model.
*/
default TextToSpeechOptions getOptions() {
// Default implementation
}
}StreamingTextToSpeechModel
StreamingTextToSpeechModel インターフェースは、リアルタイムでオーディオをストリーミングするためのメソッドを提供します。
@FunctionalInterface
public interface StreamingTextToSpeechModel extends StreamingModel<TextToSpeechPrompt, TextToSpeechResponse> {
/**
* Streams text-to-speech responses with metadata.
*/
Flux<TextToSpeechResponse> stream(TextToSpeechPrompt prompt);
/**
* Streams audio bytes for the given text.
*/
default Flux<byte[]> stream(String text) {
// Default implementation
}
}プロバイダーに依存しないコードを書く
共有 TTS インターフェースの主な利点の一つは、変更を加えることなくあらゆる TTS プロバイダーで動作するコードを記述できることです。実際のプロバイダー(OpenAI、ElevenLabs など)は Spring Boot の設定によって決定されるため、アプリケーションコードを変更することなくプロバイダーを切り替えることができます。
基本的なサービス例
共有インターフェースを使用すると、任意の TTS プロバイダーで動作するコードを記述できます。
@Service
public class NarrationService {
private final TextToSpeechModel textToSpeechModel;
public NarrationService(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
public byte[] narrate(String text) {
// Works with any TTS provider
return textToSpeechModel.call(text);
}
public byte[] narrateWithOptions(String text, TextToSpeechOptions options) {
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text, options);
TextToSpeechResponse response = textToSpeechModel.call(prompt);
return response.getResult().getOutput();
}
}このサービスは、OpenAI、ElevenLabs、その他の TTS プロバイダーとシームレスに連携し、実際の実装は Spring Boot の構成によって決まります。
高度な例: マルチプロバイダーサポート
複数の TTS プロバイダーを同時にサポートするアプリケーションを構築できます。
@Service
public class MultiProviderNarrationService {
private final Map<String, TextToSpeechModel> providers;
public MultiProviderNarrationService(List<TextToSpeechModel> models) {
// Spring will inject all available TextToSpeechModel beans
this.providers = models.stream()
.collect(Collectors.toMap(
model -> model.getClass().getSimpleName(),
model -> model
));
}
public byte[] narrateWithProvider(String text, String providerName) {
TextToSpeechModel model = providers.get(providerName);
if (model == null) {
throw new IllegalArgumentException("Unknown provider: " + providerName);
}
return model.call(text);
}
public Set<String> getAvailableProviders() {
return providers.keySet();
}
}ストリーミングオーディオの例
共有インターフェースは、リアルタイムオーディオ生成のためのストリーミングもサポートします。
@Service
public class StreamingNarrationService {
private final TextToSpeechModel textToSpeechModel;
public StreamingNarrationService(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
public Flux<byte[]> streamNarration(String text) {
// TextToSpeechModel extends StreamingTextToSpeechModel
return textToSpeechModel.stream(text);
}
public Flux<TextToSpeechResponse> streamWithMetadata(String text, TextToSpeechOptions options) {
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text, options);
return textToSpeechModel.stream(prompt);
}
}REST コントローラーの例
プロバイダーに依存しない TTS を使用した REST API の構築:
@RestController
@RequestMapping("/api/tts")
public class TextToSpeechController {
private final TextToSpeechModel textToSpeechModel;
public TextToSpeechController(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
@PostMapping(value = "/synthesize", produces = "audio/mpeg")
public ResponseEntity<byte[]> synthesize(@RequestBody SynthesisRequest request) {
byte[] audio = textToSpeechModel.call(request.text());
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("audio/mpeg"))
.header("Content-Disposition", "attachment; filename=\"speech.mp3\"")
.body(audio);
}
@GetMapping(value = "/stream", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Flux<byte[]> streamSynthesis(@RequestParam String text) {
return textToSpeechModel.stream(text);
}
record SynthesisRequest(String text) {}
} ストリームをブロッキング InputStream として消費する
一部の API は、リアクティブな Flux ではなく、java.io.InputStream のみを受け入れます。オーディオ再生ライブラリや、ブロッキング I/O を使用してディスクに増分的に書き込むコードなどが典型的な例です。Flux<byte[]> を InputStream にブリッジするのは簡単ですが、そのようなブリッジが実際に何を保証するのかを正確に理解しておくことが重要です。
適切に動作するブリッジは、Flux から一度に 1 つのチャンクをリクエストする必要があります。そのため、InputStream コンシューマーが読み取ったチャンクよりも先に、すでに受信した 1 つのチャンク以上をバッファリングすることはありません。また、ストリームの終了に達する前に InputStream が閉じられた場合は、基となるサブスクリプションをキャンセルする必要があります。これにより、再生を中止するなど、早期に停止するコンシューマーは、レスポンスが完了するまでプロバイダー接続を実行したままにするのではなく、プロバイダー接続をすぐに解放します。Flux#toIterable() 上に構築されたブリッジは、イテレータがサブスクリプションをキャンセルする方法を公開していないため、この 2 番目の特性を提供できません。reactor.core.publisher.BaseSubscriber を介してブリッジングすると、hookOnNext から一度に 1 つの要素をリクエストし、Subscriber.dispose() を介して close() からキャンセルするという、両方の特性が得られます。
InputStream コンシューマーを一時停止すると、OpenAI のように HTTP/2 を使用するクライアントを持つプロバイダに実際のバックプレッシャーが伝播します。基盤となるトランスポートである OkHttp は、アプリケーションコードがバッファリングされたバイトを実際に読み取った後にのみ、サーバーにさらにデータを送信するように指示する WINDOW_UPDATE フレームを送信するため、停止したコンシューマーは最終的にその接続でサーバーがそれ以上のデータを送信するのを停止させます。この効果は確かに存在しますが、粒度が粗く、単一の未読チャンクの後ではなく、HTTP/2 フロー制御ウィンドウ全体分の未読データが蓄積された後(通常は数十キロバイト)に発生します。また、トランスポートが送信を停止することのみを保証します。特定のプロバイダのバックエンドが、送信バッファがいっぱいになったときに、サーバー側でバッファリングを続けるのではなく、実際にオーディオの生成を減速するかどうかはプロバイダ固有の問題であり、クライアント側のブリッジが制御または検証できるものではありません。
完全かつテスト済みの参照実装である FluxInputStream は、OpenAiAudioSpeechModelIT の spring-ai-openai モジュールのテストソースに含まれています。 |
構成ベースのプロバイダー選択
Spring プロファイルまたはプロパティを使用してプロバイダーを切り替えます。
# application-openai.yml
spring:
ai:
model:
audio:
speech: openai
openai:
api-key: ${OPENAI_API_KEY}
audio:
speech:
options:
model: gpt-4o-mini-tts
voice: alloy
# application-elevenlabs.yml
spring:
ai:
model:
audio:
speech: elevenlabs
elevenlabs:
api-key: ${ELEVENLABS_API_KEY}
tts:
options:
model-id: eleven_turbo_v2_5
voice-id: your_voice_id次に、希望するプロバイダーをアクティブ化します。
# Use OpenAI
java -jar app.jar --spring.profiles.active=openai
# Use ElevenLabs
java -jar app.jar --spring.profiles.active=elevenlabsポータブルオプションの使用
移植性を最大限に高めるには、一般的な TextToSpeechOptions インターフェースメソッドのみを使用します。
@Service
public class PortableNarrationService {
private final TextToSpeechModel textToSpeechModel;
public PortableNarrationService(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
public byte[] createPortableNarration(String text) {
// Use provider's default options for maximum portability
TextToSpeechOptions options = textToSpeechModel.getOptions();
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text, options);
TextToSpeechResponse response = textToSpeechModel.call(prompt);
return response.getResult().getOutput();
}
}プロバイダー固有の機能の操作
プロバイダー固有の機能が必要な場合でも、移植可能なコードベースを維持しながら使用できます。
@Service
public class FlexibleNarrationService {
private final TextToSpeechModel textToSpeechModel;
public FlexibleNarrationService(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
public byte[] narrate(String text, TextToSpeechOptions baseOptions) {
TextToSpeechOptions options = baseOptions;
// Apply provider-specific optimizations if available
if (textToSpeechModel instanceof OpenAiAudioSpeechModel) {
options = OpenAiAudioSpeechOptions.builder()
.from(baseOptions)
.model("gpt-4o-tts") // OpenAI-specific: use high-quality model
.speed(1.0)
.build();
} else if (textToSpeechModel instanceof ElevenLabsTextToSpeechModel) {
// ElevenLabs-specific options could go here
}
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text, options);
TextToSpeechResponse response = textToSpeechModel.call(prompt);
return response.getResult().getOutput();
}
}ポータブルコードのベストプラクティス
インターフェースに依存する : 具体的な実装ではなく、常に
TextToSpeechModelを注入する共通オプションを使用する : 移植性を最大限に高めるには、
TextToSpeechOptionsインターフェース方式を採用するメタデータを適切に処理する : プロバイダによって返されるメタデータは異なるため、汎用的に処理する
複数のプロバイダーでテストする : コードが少なくとも 2 つの TTS プロバイダーで動作することを確認する
ドキュメントプロバイダーの想定 : 特定のプロバイダーの行動に依存する場合は、それを明確にドキュメント化します。