跳到主要内容
版本:2.0.1

文本转语音 (TTS) API

QWen 3.8 中英对照 Text-To-Speech (TTS) API

Spring AI 通过 TextToSpeechModel 和 StreamingTextToSpeechModel 接口提供了统一的文本转语音(Text-To-Speech,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
}
}

TextToSpeechPrompt​

TextToSpeechPrompt 类封装了输入文本和选项:

TextToSpeechPrompt prompt = new TextToSpeechPrompt(
"Hello, this is a text-to-speech example.",
options
);

TextToSpeechResponse​

TextToSpeechResponse 类包含生成的音频和元数据:

TextToSpeechResponse response = model.call(prompt);
byte[] audioBytes = response.getResult().getOutput();
TextToSpeechResponseMetadata metadata = response.getMetadata();

编写与提供商无关的代码​

共享 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 仅接受 java.io.InputStream,而非响应式 Flux。典型的示例包括音频播放库,以及使用阻塞 I/O 将数据增量写入磁盘的代码。将 Flux<byte[]> 桥接为 InputStream 的过程很直接,但有必要精确了解此类桥接实际保证了什么。

表现良好的桥接器应从 Flux 中每次请求一个数据块(chunk),从而确保不会缓冲超过单个已接收的数据块——即始终保持在 InputStream 消费者已读取内容的前面不多不少一个数据块。此外,当 InputStream 在到达流末尾(end-of-stream)之前被关闭时,它也应取消底层的订阅。这样,如果消费者提前停止(例如中止播放),就能立即释放提供者连接,而不是让其继续运行,直到响应本应完成为止。基于 Flux#toIterable() 构建的桥接器无法提供后一种特性,因为其迭代器没有暴露任何取消订阅的方式。通过桥接 reactor.core.publisher.BaseSubscriber 则可以同时具备这两种特性:在 hookOnNext 中每次请求一个元素,并在 close() 中通过 Subscriber.dispose() 执行取消操作。

暂停 InputStream 消费者会将真实的反压传播给使用 HTTP/2 的提供商(例如 OpenAI)。底层的传输层 OkHttp 只有在应用代码实际读取了缓冲的字节后,才会发送告知服务器发送更多数据的 WINDOW_UPDATE 帧,因此一个停滞的消费者最终会阻止服务器在该连接上继续发送进一步的数据。这种效果是真实存在的,但粒度较粗:它是在积累了整个 HTTP/2 流控制窗口大小的未读数据(通常是数十千字节)时才触发,而不是在单个未读块后就触发。此外,它还仅保证传输层停止发送;至于特定提供商的后端在其发送缓冲区填满时是真正减缓音频生成,还是继续在服务器端缓冲,这取决于提供商的具体实现,并非客户端桥接层所能控制或验证。

提示

在 spring-ai-openai 模块的测试源代码中,OpenAiAudioSpeechModelIT 提供了完整的、经过测试的参考实现 FluxInputStream。

基于配置的提供者选择​

使用 Spring profiles 或 properties 在 providers 之间进行切换:

# 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

然后激活所需的 provider:

# 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();
}
}

可移植代码的最佳实践​

  1. 依赖接口:始终注入 TextToSpeechModel 而非具体实现类

  2. 使用通用选项:坚持使用 TextToSpeechOptions 接口方法,以获得最大的可移植性

  3. 优雅地处理元数据:不同的提供商返回不同的元数据,请对其进行通用处理

  4. 使用多个提供商进行测试:确保您的代码至少能与两个 TTS 提供商兼容

  5. 记录提供商假设:如果您依赖于特定提供商的行为,请明确记录

特定提供商功能​

尽管共享接口提供了可移植性,但每个提供商也通过提供商特定的选项类(例如,OpenAiAudioSpeechOptions、ElevenLabsSpeechOptions)提供了特定功能。这些类实现了 TextToSpeechOptions 接口,同时增加了提供商特定的功能。

小节概要​