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

简介: 本文围绕 Spring AI Alibaba 1.1.2.2 的文本转语音实现展开,记录了基于 DashScopeAudioSpeechModel 与 stream() 的可运行方案。文章重点说明了模型、音色、输出格式与流式拼接音频文件的关键细节。

第十章 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 的具体实现类:

  • DashScopeAudioSpeechModel
  • DashScopeAudioSpeechOptions

原因很现实: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() 的思路去写。

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

  1. 显式指定模型和音色:确保模型版本和音色是匹配的组合
  2. 显式指定输出格式:通过 .format("mp3") 告诉服务端返回 MP3 音频流
  3. 手动拼接音频块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 能力基本就可以直接作为最后的“发声出口”。

本章重点

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

下章剧透(s11):

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


💡 TIP:从 TTS 到 Voice Agent

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

语音输入 -> 语音识别(STT) -> Agent 推理 -> 文本转语音(TTS)

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


📝 编辑者:Flittly
📅 更新时间:2026年4月

目录
相关文章
|
4天前
|
人工智能 前端开发 Java
【SpringAIAlibaba新手村系列】(4)流式输出与响应式编程
本文围绕 Spring AI 中的流式输出与响应式编程展开,重点解释了传统一次性响应与流式返回的差异,以及 Flux 在异步数据流中的核心作用。文章结合 ChatModel.stream() 与 ChatClient 的多种代码示例,说明如何实现 AI 内容的边生成边返回,并帮助读者理解流式调用在用户体验、性能和长文本场景中的实际价值。
148 4
【SpringAIAlibaba新手村系列】(4)流式输出与响应式编程
|
1天前
|
存储 人工智能 JSON
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
为解决OpenClaw等AI Agent“Done”回复背后的黑盒问题,我们基于DuckDB开发了轻量可观测插件:通过Hook采集关键节点事件,建模为结构化Trace链路,异步写入本地或云上DuckDB,提供瀑布图式执行视图、指标分析与安全告警,让Agent从不可见变为可追踪、可解释、可优化。
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
|
9天前
|
存储 JSON 开发工具
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(12)Worktree + Task Isolation (Worktree 任务隔离)
本章引入 Git worktree 实现任务隔离:为每个任务分配独立目录与分支,通过任务 ID 双向绑定控制平面(`.tasks/`)与执行平面(`.worktrees/`),配合事件总线记录全生命周期,解决多任务并发修改冲突问题,提升可恢复性与可观测性。
407 0
|
27天前
|
存储 人工智能 Shell
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(3)TodoWrite (待办写入)
本章详解 s03 版本 TodoWrite 机制:通过 `todo` 工具+`TodoManager` 实现显式任务状态管理(pending/in_progress/completed),强制单任务聚焦;并引入“nag 提醒”——连续3轮未更新待办时自动注入提醒,解决大模型长链路任务健忘问题。代码精简可运行。
390 3
|
存储 人工智能 Java
【SpringAIAlibaba新手村系列】(3)ChatModel 与 ChatClient 的深度对比
本章深度解析 Spring AI 中 `ChatModel`(底层接口)与 `ChatClient`(高级封装)的本质区别:前者如“手动挡”,精准控制但需写大量样板代码;后者似“智能点餐机”,链式调用、支持系统提示、模板、工具调用等,开发高效。初学者推荐优先使用 `ChatClient`。
143 0
【SpringAIAlibaba新手村系列】(3)ChatModel 与 ChatClient 的深度对比
|
14天前
|
人工智能 JSON 安全
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(9)Agent Teams (智能体团队)
本章构建持久化智能体团队,解决一次性子智能体和哑后台任务的问题。每个队友拥有独立线程与 LLM 循环,通过 JSONL 文件邮箱实现跨轮通信;TeammateManager 维护团队名册与生命周期,MessageBus 提供线程安全的消息收发,支持并行协作、任务分派与状态同步,为大型项目多 Agent 协同奠定基础。
504 4
|
14天前
|
人工智能 JSON 监控
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(10)Team Protocols (团队协议)
本章详解AI团队协议设计:通过request-id、状态机(pending→approved/rejected)和内存追踪器,实现关机握手与计划审批两大结构化交互。告别s09的随意消息,构建可追溯、可审批、线程安全的协作范式。
106 7
|
人工智能 Java Spring
【SpringAIAlibaba新手村系列】(2)Ollama 本地大模型调用
本章详解Ollama本地大模型调用:解决联网依赖、数据隐私与成本问题。通过`spring-ai-ollama-starter`快速集成,支持Qwen、Llama等开源模型,实现零费用、低延迟、高安全的本地AI服务,并深入剖析`@Qualifier`多Bean注入机制。
365 0
|
Arthas Kubernetes 数据可视化
推荐10个GitHub上适合练手的后端项目(涵盖初中高阶)
上周,我们推出了26个好玩又有挑战的前端练习项目。 不少同学留言说,那后端的呢?后端也要! 淘系工程师一呼就应,我们邀请了2位淘系技术后端工程师,筛选出10个难度层层递进,好玩且实用的后端项目,包含java类库中的“瑞士军刀”工具、可视化API展现等等,难度依然分为【初级篇:4个】、【中级篇:3个】、【高级篇:3个】,不同学习诉求的同学可按需选择~
推荐10个GitHub上适合练手的后端项目(涵盖初中高阶)
|
5月前
|
人工智能 双11 开发者
中小电商卖家福音!用AI 3分钟搞定高转化主图+详情页!
还在为电商设计成本高、效率低发愁?阿里云百炼全新推出「一键生成电商组图」功能,AI智能生成主图、详情页等视觉素材,零设计基础也能快速出图!限时参与活动,赢定制好礼,更有超值Tokens优惠包,助力爆款打造!

热门文章

最新文章