第十章 Text to Voice 文本转语音技术
版本标注
- Spring AI:
1.1.2- Spring AI Alibaba:
1.1.2.2章节定位
- 本章聚焦 TTS 基础调用,也就是“把文字转成语音”。
- 在更完整的语音应用里,它通常会进一步组合成
STT -> Agent -> TTS的语音交互链路。
s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > [ s10 ] s11 > s12 > s13 > s14 > s15 > s16 > s17 > s18
"文字一旦开口说话, 应用场景立刻就扩展了" -- TTS 连接的是文本理解和真实交互。
一、什么是 Text to Voice?
1.1 概念科普
Text to Voice / Text to Speech(TTS)说白了,就是把一段文字直接变成可播放的语音。我自己学到这一章时最直观的感受是:前面的聊天接口终于开始“开口说话”了,整个应用一下子就从文本工具变成了语音交互应用。
应用场景:
- 听书听新闻
- 语音播报通知
- 辅助视障人士
- 智能客服语音
- 有声内容创作
1.2 阿里云 CosyVoice
本章最终跑通时,我采用的是阿里云的 CosyVoice v3 Flash 模型。它的几个特点很适合拿来做学习和演示:
- 中文发音自然流畅
- 首包延迟低,适合实时语音输出
- 支持流式返回音频数据
二、TextToSpeech 相关核心类
2.1 DashScopeAudioSpeechModel
在 Spring AI Alibaba 1.1.2.2 里,我最后采用的是下面这个语音模型实现类:
com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechModel
它在这一章里可以简单理解成“真正负责把文字送去合成语音”的那个对象。它做的事情主要有三件:
- 接收
TextToSpeechPrompt - 调用 DashScope 的 TTS 服务
- 以流式方式返回音频数据
这里要特别注意:
当前版本下,像
cosyvoice-v3-flash这类模型通常应通过stream()使用,而不是call()。
2.2 TextToSpeechPrompt
TextToSpeechPrompt 就是一次语音合成请求本身。你可以把它理解成“这次我要读什么内容、用什么参数去读”的打包对象。
// 创建语音合成请求
TextToSpeechPrompt prompt = new TextToSpeechPrompt(
"你好,我是AI助手", // 要转换的文字
options // 语音选项
);
2.3 DashScopeAudioSpeechOptions
DashScopeAudioSpeechOptions 则是这次语音生成的参数区。模型、音色、输出格式、采样率,基本都放在这里配置。
// 构建语音选项
DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder()
.model("cosyvoice-v3-flash") // 语音模型
.voice("longanyang") // 音色选择
.format("mp3") // 输出格式
.sampleRate(22050) // 采样率
.textType("PlainText") // 文本类型
.build();
三、项目代码详解
3.1 依赖配置说明
3.1.1 当前章节采用的类路径
这一章最后我固定采用的 TTS 实现类路径是:
com.alibaba.cloud.ai.dashscope.audio.tts
也就是说,控制器代码中的核心类来自:
import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechModel;
import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechOptions;
我这里最终就按这套来写,因为它和当前 1.1.2.2 的实际运行结果是对上的。
3.1.2 本章所需依赖
本章最终能正常跑通,依赖上我保留的是下面这两个:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
</dependency>
可以把它们理解成一层“自动装配”和一层“具体实现”:
spring-ai-alibaba-starter-dashscope:提供 Spring Boot 自动配置能力spring-ai-alibaba-dashscope:提供更底层的 DashScope 具体实现类,包括语音合成相关实现
3.1.3 推荐依赖写法
如果父工程已经通过 BOM 管理版本,子模块里这样写就够了:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
</dependency>
如果你的模块和我一样,偶尔会遇到 BOM 没有稳定接管版本的问题,那就直接显式把版本写出来:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
<version>1.1.2.2</version>
</dependency>
3.1.4 一个更稳的理解方式
这一章我最后没有再去兜圈子追求“最抽象的写法”,而是直接使用 DashScope 的具体实现类:
DashScopeAudioSpeechModelDashScopeAudioSpeechOptions
原因很现实:TTS 这块在不同版本里的 API 变化比普通对话接口更快,直接用具体实现类,反而更容易把模型、音色、格式和流式输出这些细节对齐。
3.2 控制器代码
package com.atguigu.study.controller;
import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechModel;
import com.alibaba.cloud.ai.dashscope.audio.tts.DashScopeAudioSpeechOptions;
import com.alibaba.cloud.ai.dashscope.spec.DashScopeModel;
import org.springframework.ai.audio.tts.TextToSpeechPrompt;
import org.springframework.ai.audio.tts.TextToSpeechResponse;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.io.FileOutputStream;
import java.util.List;
import java.util.UUID;
/**
* 文本转语音控制器
* 展示如何将文字转换为语音(MP3格式)
*/
@RestController
public class Text2VoiceController
{
@Resource(name = "dashScopeSpeechSynthesisModel")
private DashScopeAudioSpeechModel speechModel;
public static final String BAILIAN_VOICE_MODEL = DashScopeModel.AudioModel.COSYVOICE_V3_FLASH.getValue();
public static final String BAILIAN_VOICE_TIMBER = "longanyang";
/**
* 文本转语音
*
* 接口:http://localhost:8010/t2v/voice?msg=温馨提醒,支付宝到账100元请注意查收
*
* @param msg 要转成语音的文字
* @return 生成的语音文件路径
*/
@GetMapping("/t2v/voice")
public String voice(@RequestParam(name = "msg", defaultValue = "温馨提醒,支付宝到账100元请注意查收") String msg)
{
String filePath = System.getProperty("java.io.tmpdir") + UUID.randomUUID() + ".mp3";
DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder()
.model(BAILIAN_VOICE_MODEL)
.voice(BAILIAN_VOICE_TIMBER)
.format("mp3")
.sampleRate(22050)
.textType("PlainText")
.build();
TextToSpeechPrompt prompt = new TextToSpeechPrompt(msg, options);
byte[] audioBytes = collectStreamBytes(speechModel.stream(prompt));
if (audioBytes == null || audioBytes.length == 0) {
throw new IllegalStateException("TTS generated no audio data");
}
try (FileOutputStream fileOutputStream = new FileOutputStream(filePath))
{
fileOutputStream.write(audioBytes);
} catch (Exception e) {
throw new RuntimeException("Failed to write audio file", e);
}
return filePath;
}
private byte[] collectStreamBytes(Flux<TextToSpeechResponse> stream) {
List<byte[]> chunks = stream
.filter(r -> r != null && r.getResult() != null && r.getResult().getOutput() != null)
.map(r -> r.getResult().getOutput())
.collectList()
.block();
if (chunks == null || chunks.isEmpty()) {
return new byte[0];
}
int total = chunks.stream().mapToInt(b -> b.length).sum();
byte[] result = new byte[total];
int offset = 0;
for (byte[] chunk : chunks) {
System.arraycopy(chunk, 0, result, offset, chunk.length);
offset += chunk.length;
}
return result;
}
}
3.3 为什么要这样写?
这一章真正绕人的地方,不在于“怎么把字节写进文件”,而在于当前版本的语音模型应该怎么调用。我最后跑通以后,结论其实很明确:cosyvoice-v3-flash 这类模型更适合走 stream(),不适合再按传统同步 call() 的思路去写。
所以这个实现真正要抓住的是三个点:
- 显式指定模型和音色:确保模型版本和音色是匹配的组合
- 显式指定输出格式:通过
.format("mp3")告诉服务端返回 MP3 音频流 - 手动拼接音频块:
stream()返回的是多个音频分片,需要把所有byte[]顺序拼接后,才能写成完整文件
这几个参数里,最容易忽略但又最关键的是:
.sampleRate(22050)用来约定输出采样率.textType("PlainText")用来明确当前输入是普通文本而不是其他格式collectStreamBytes(...)的作用,就是把多个流式分片还原成完整音频字节数组
所以最后这段代码的思路就变成了:不是等服务端一次性把完整文件塞回来,而是先收集流式返回的音频块,再在本地把它们拼成完整文件。
四、音色选择与参数调整
4.1 可用音色列表
实际写代码时,音色最好不要随便猜。我这里先列几个常见音色,够做学习和实验用了:
| 音色名称 | 音色描述 | 适用场景 |
|---|---|---|
| longanyang | 龙阳 | 标准男声 |
| xiaoyuan | 小圆满 | 清亮女声 |
| yaying | 雅音 | 温柔女声 |
| zhishengtts | 致远 | 标准男声 |
4.2 参数调优
// 调整语速
.withSpeed(0.8) // 0.5-2.0,越小越慢,越大越快
// 调整音量
.withVolume(1.2) // 0.1-10.0,默认1.0
// 调整音调
.withPitch(2.0) // -12.0到12.0,正值偏高,负值偏低
五、音频播放与后续处理
5.1 在前端播放
后端把文件生成出来后,前端最直接的做法就是用 HTML5 的 <audio> 标签播放:
<!-- 直接播放 -->
<audio controls>
<source src="http://localhost:8010/audio/xxx.mp3" type="audio/mpeg">
</audio>
<!-- 或者用 JavaScript -->
<script>
new Audio('http://localhost:8010/audio/xxx.mp3').play();
</script>
5.2 生成播放链接
如果需要提供 HTTP 访问,可以配置静态资源或文件服务:
# application.yml
spring:
web:
resources:
static-locations: file:d:/,classpath:/static/
然后在控制器里返回一个可访问的 URL,而不是磁盘绝对路径。
六、本章小结
6.1 核心概念
| 概念 | 说明 |
|---|---|
| DashScopeAudioSpeechModel | 语音合成的核心模型 |
| TextToSpeechPrompt | 语音合成的请求对象 |
| CosyVoice | 阿里云语音合成模型 |
| Flux | 流式返回的语音分片 |
| byte[] | 最终拼接后的完整音频数据 |
6.2 使用流程
1. 准备要转换的文字
2. 创建语音选项(模型、音色、语速等)
3. 生成语音合成请求
4. 调用 `speechModel.stream(prompt)` 获取流式语音分片
5. 拼接多个 `byte[]` 分片为完整音频
6. 将完整音频写入文件
6.3 注意事项
- 生成的音频文件最好放到临时目录、对象存储或统一文件服务里管理,不建议长期直接落在本地磁盘根目录
- 音色不要只看名字,最好和当前模型版本一起确认
- 如果后面发现文件能生成但播放器打不开,优先先检查输出格式、采样率和流式拼接逻辑
如果后面继续往语音 Agent 方向扩展,这一章的 TTS 能力基本就可以直接作为最后的“发声出口”。
本章重点:
- 掌握
DashScopeAudioSpeechModel.stream()的使用方法 - 理解流式音频分片如何拼接为完整文件
- 能够正确配置模型、音色、输出格式和采样率
下章剧透(s11):
学会了文字生成语音后,下一章我们将学习 Embedding(向量化)——让 AI 理解文本的数学表示,这也是 RAG 技术的核心基础!
💡 TIP:从 TTS 到 Voice Agent
本章解决的是 文本转语音 这一小段链路。但真实的语音交互应用通常还会继续向前补一段:
语音输入 -> 语音识别(STT) -> Agent 推理 -> 文本转语音(TTS)
你可以把本章学到的 DashScopeAudioSpeechModel 直接看成这条语音链路中的最后一环。
📝 编辑者:Flittly
📅 更新时间:2026年4月