【SpringAIAlibaba新手村系列】(10)Text to Voice 文本转语音技术

第十章 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 就是一次语音合成请求本身。你可以把它理解成"这次我要读什么内容、用什么参数去读"的打包对象。

java 复制代码
// 创建语音合成请求
TextToSpeechPrompt prompt = new TextToSpeechPrompt(
    "你好,我是AI助手",      // 要转换的文字
    options                 // 语音选项
);

2.3 DashScopeAudioSpeechOptions

DashScopeAudioSpeechOptions 则是这次语音生成的参数区。模型、音色、输出格式、采样率,基本都放在这里配置。

scss 复制代码
// 构建语音选项
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

也就是说,控制器代码中的核心类来自:

arduino 复制代码
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 本章所需依赖

本章最终能正常跑通,依赖上我保留的是下面这两个:

xml 复制代码
<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 管理版本,子模块里这样写就够了:

xml 复制代码
<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 没有稳定接管版本的问题,那就直接显式把版本写出来:

xml 复制代码
<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 的具体实现类:

  • DashScopeAudioSpeechModel
  • DashScopeAudioSpeechOptions

原因很现实:TTS 这块在不同版本里的 API 变化比普通对话接口更快,直接用具体实现类,反而更容易把模型、音色、格式和流式输出这些细节对齐。

3.2 控制器代码

ini 复制代码
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() 的思路去写。

所以这个实现真正要抓住的是三个点:

  1. 显式指定模型和音色:确保模型版本和音色是匹配的组合
  2. 显式指定输出格式 :通过 .format("mp3") 告诉服务端返回 MP3 音频流
  3. 手动拼接音频块stream() 返回的是多个音频分片,需要把所有 byte[] 顺序拼接后,才能写成完整文件

这几个参数里,最容易忽略但又最关键的是:

  • .sampleRate(22050) 用来约定输出采样率
  • .textType("PlainText") 用来明确当前输入是普通文本而不是其他格式
  • collectStreamBytes(...) 的作用,就是把多个流式分片还原成完整音频字节数组

所以最后这段代码的思路就变成了:不是等服务端一次性把完整文件塞回来,而是先收集流式返回的音频块,再在本地把它们拼成完整文件。


四、音色选择与参数调整

4.1 可用音色列表

实际写代码时,音色最好不要随便猜。我这里先列几个常见音色,够做学习和实验用了:

音色名称 音色描述 适用场景
longanyang 龙阳 标准男声
xiaoyuan 小圆满 清亮女声
yaying 雅音 温柔女声
zhishengtts 致远 标准男声

4.2 参数调优

scss 复制代码
// 调整语速
.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> 标签播放:

xml 复制代码
<!-- 直接播放 -->
<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 访问,可以配置静态资源或文件服务:

yaml 复制代码
# application.yml
spring:
  web:
    resources:
      static-locations: file:d:/,classpath:/static/

然后在控制器里返回一个可访问的 URL,而不是磁盘绝对路径。


六、本章小结

6.1 核心概念

概念 说明
DashScopeAudioSpeechModel 语音合成的核心模型
TextToSpeechPrompt 语音合成的请求对象
CosyVoice 阿里云语音合成模型
Flux 流式返回的语音分片
byte[] 最终拼接后的完整音频数据

6.2 使用流程

markdown 复制代码
1. 准备要转换的文字
2. 创建语音选项(模型、音色、语速等)
3. 生成语音合成请求
4. 调用 `speechModel.stream(prompt)` 获取流式语音分片
5. 拼接多个 `byte[]` 分片为完整音频
6. 将完整音频写入文件

6.3 注意事项

  • 生成的音频文件最好放到临时目录、对象存储或统一文件服务里管理,不建议长期直接落在本地磁盘根目录
  • 音色不要只看名字,最好和当前模型版本一起确认
  • 如果后面发现文件能生成但播放器打不开,优先先检查输出格式、采样率和流式拼接逻辑

如果后面继续往语音 Agent 方向扩展,这一章的 TTS 能力基本就可以直接作为最后的"发声出口"。

本章重点

  1. 掌握 DashScopeAudioSpeechModel.stream() 的使用方法
  2. 理解流式音频分片如何拼接为完整文件
  3. 能够正确配置模型、音色、输出格式和采样率

下章剧透(s11):

学会了文字生成语音后,下一章我们将学习 Embedding(向量化)------让 AI 理解文本的数学表示,这也是 RAG 技术的核心基础!


💡 TIP:从 TTS 到 Voice Agent

本章解决的是 文本转语音 这一小段链路。但真实的语音交互应用通常还会继续向前补一段:

rust 复制代码
语音输入 -> 语音识别(STT) -> Agent 推理 -> 文本转语音(TTS)

你可以把本章学到的 DashScopeAudioSpeechModel 直接看成这条语音链路中的最后一环。


📝 编辑者 :Flittly

📅 更新时间:2026年4月

相关推荐
JoshRen2 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
诸葛大钢铁2 小时前
Java实现Excel文件合并
java·windows·excel
黎明丶之前2 小时前
Spring Cloud Gateway 升级与 Bucket4j 限流实践
java·spring cloud
洛卡卡了2 小时前
别人开盲盒我开源码:我的 Claude Code 宠物是怎么变成金色传说龙的
agent·ai编程·claude
程序员木圭2 小时前
05-告别逻辑混乱!Java 流程控制让代码学会"判断和循环"
java·后端
MmeD UCIZ2 小时前
Skywalking介绍,Skywalking 9.4 安装,SpringBoot集成Skywalking
spring boot·后端·skywalking
ward RINL2 小时前
Spring boot启动原理及相关组件
数据库·spring boot·后端
yaaakaaang2 小时前
三、抽象工厂模式
java·抽象工厂模式
kongba0072 小时前
复刻 Claude Code 项目御马术缰绳系统 harness engineering 落地蓝图
java·linux·服务器