当前位置: 首页 > news >正文

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

// 创建语音合成请求 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月

http://www.cnnetsun.cn/news/1660989.html

相关文章:

  • OpenClaw数据清洗实战:Phi-3-mini-128k-instruct处理混乱Excel
  • Nanbeige4.1-3B开源镜像免配置:Llama架构+bf16量化,中小企业低成本AI部署方案
  • 开源工具Wand-Enhancer功能增强技术解析与实战指南
  • CSS 样式细节:如何在特定情况下添加删除线
  • PL-2303串口驱动Windows 10兼容性问题深度解决方案:从设备识别到稳定通讯的全流程修复
  • lite-avatar形象库效果展示:教师数字人在直播授课场景中的眼神交互与手势模拟
  • TCP吞吐瓶颈分析与Wireshark调优实战
  • Open Interpreter异常处理机制:错误捕获与修复实战
  • OpenClaw多模态创意工具:千问3.5-35B-A3B-FP8根据手绘草图生成产品设计文档
  • 2025届必备的六大降AI率助手推荐
  • 2026年绩效考核系统的三个关键变化
  • AOT编译不是“编译即省”!Python原生AOT成本失控的5个隐性黑洞,92%团队第3个就误判
  • 3 个高级思路,让你的 AI 绘画 / 视频从此充满想象力
  • OpenClaw飞书机器人配置:Qwen3.5-9B-AWQ-4bit对话触发全流程
  • 电机轴承异响?5分钟教你用振动分析仪定位故障(附实测案例)
  • NPS内网穿透实战避坑:从Web管理后台打不开到客户端连不上的5个常见问题解决
  • 嵌入式系统中的事件驱动型有限状态机(EFSM)设计与实现
  • 用九齐单片机NY8B062F定时器实现精准延时与系统时基:从4ms中断到1秒计时的完整工程实践
  • 5款智能电视文件管理器深度体验:功能对比与适用场景解析
  • Grafana+InfluxDB监控系统搭建全攻略:从安装到可视化大屏设计
  • SEO_移动端SEO优化的关键步骤与注意事项(237 )
  • 零代码玩转多模态AI:OpenClaw+Kimi-VL-A3B-Thinking自动化图文博客生成
  • 【程序源代码】点餐外卖系统设计与实现
  • 破局与重构:金融行业新一代 IT 基础架构的全景演进与落地指南(PPT)
  • 保姆级教程:手把手教你用百度网盘下载并安装AD20(附汉化与激活全流程)
  • 自动化立体库堆垛机效率优化——基于FEM9.851标准的单深单货位场景解析
  • SAM3D实战:如何用Segment Anything技术提升你的3D场景理解项目效果
  • 从ASCII码到Word模板:深入理解Apache POI中(char)11这个‘竖直制表符’的妙用
  • 家庭知识库中心:OpenClaw+Qwen3.5-9B管理个人数字资产
  • 职场选择与职业发展:半导体工程师的路径规划