阿里云开发一个文本转语音的网页(Springboot+vue)
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
·
演示视频请点击查看B站视频
数据库文件和源代码请看这里
源代码请点击
0首先你需要有阿里云账号并且开通语音服务
有免费试用详情见官网的步骤开通语音
1页面展示


2前端
因为我是用的人人fast框架搭建的,所以大家只需要看页面的布局(主要是elementUI),数据的处理就行,很简单就可以套用到自己的页面中
2.1列表页 voice.vue
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('voice:voice:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('voice:voice:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column
type="selection"
header-align="center"
align="center"
width="50">
</el-table-column>
<el-table-column
prop="id"
header-align="center"
align="center"
label="ID">
</el-table-column>
<el-table-column
prop="content"
header-align="center"
align="center"
label="文本">
<template slot-scope="scope2">
<el-popover
placement="bottom"
title="语音内容"
width="200"
trigger="click"
:content="scope2.row.content">
<el-button slot="reference">查看</el-button>
</el-popover>
</template>
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
label="发音人">
</el-table-column>
<el-table-column
prop="samplerate"
header-align="center"
align="center"
label="采样频率">
</el-table-column>
<el-table-column
prop="pitchrate"
header-align="center"
align="center"
label="语调">
</el-table-column>
<el-table-column
prop="speechrate"
header-align="center"
align="center"
label="语速">
</el-table-column>
<el-table-column
prop="createdate"
header-align="center"
align="center"
label="创建时间"
width="160"
>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)">删除</el-button>
<el-button type="text" @click="handlePlay(scope.row.adress)">播放</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="录音播放" :visible.sync="dialogVisible" width="30%" :before-close="stop">
<template>
<audio
:src="src"
autoplay="autoplay"
controls="controls"
ref="audio"
>Your browser does not support the audio element.</audio>
</template>
</el-dialog>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script>
import AddOrUpdate from './voice-add-or-update'
export default {
data () {
return {
src: '',
dialogVisible: false,
dataForm: {
key: ''
},
dataList: [],
pageIndex: 1,
pageSize: 10,
totalPage: 0,
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false
}
},
components: {
AddOrUpdate
},
activated () {
this.getDataList()
},
methods: {
// 获取数据列表
getDataList () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/voice/voice/list'),
method: 'get',
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'key': this.dataForm.key
})
}).then(({data}) => {
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
})
},
// 每页数
sizeChangeHandle (val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
// 当前页
currentChangeHandle (val) {
this.pageIndex = val
this.getDataList()
},
// 多选
selectionChangeHandle (val) {
this.dataListSelections = val
},
// 新增 / 修改
addOrUpdateHandle (id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// 删除
deleteHandle (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.id
})
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/voice/voice/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({data}) => {
if (data && data.code === 0) {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
})
},
// 播放组件
handlePlay (row) {
this.src = 'src/assets/voice/' + row
this.play()
},
// 播放
play () {
this.dialogVisible = true
this.$refs.audio.play()
},
// 音频暂停
stop () {
this.dialogVisible = false
this.$refs.audio.pause()
this.$refs.audio.currentTime = 0
}
}
}
</script>
值得注意的是语音播放需要安装插件
npm install vue-audio --save

新增页voice-add-or-update.vue

<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="文本" prop="content">
<el-input v-model="dataForm.content" placeholder="文本" type="textarea" maxlength="300" show-word-limit></el-input>
</el-form-item>
<!-- 阿里云的语音的发音有几十个,我只挑了具有代表性的十几个,自己开发时想要那个自己在添上-->
<el-form-item label="发音人" prop="name">
<el-select v-model="dataForm.name" placeholder="请选择" @change="isfeel()">
<el-option-group
v-for="group in options"
:key="group.label"
:label="group.label">
<el-option
v-for="item in group.options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<!-- 只有指定的语音才会有情感和情感强度显示,所以用v-if-->
<el-form-item label="情感" v-if="isfeeling">
<el-select v-model="dataForm.emo" placeholder="情感">
<el-option label="生气" value="angry"></el-option>
<el-option label="害怕" value="fear"></el-option>
<el-option label="高兴" value="happy"></el-option>
<el-option label="厌恶" value="hate"></el-option>
<el-option label="中立" value="neutral"></el-option>
<el-option label="伤心" value="sad"></el-option>
<el-option label="惊讶" value="surprise"></el-option>
</el-select>
</el-form-item>
<el-form-item label="情感强度" v-if="isfeeling">
<el-input-number v-model="dataForm.intensity" :precision="2" :step="0.01" :max="2" :min="0"></el-input-number>
</el-form-item>
<el-form-item label="采样频率" prop="samplerate">
<el-select v-model="dataForm.samplerate" placeholder="采样频率">
<el-option label="SAMPLE_RATE_16K" value="16000"></el-option>
<el-option label="SAMPLE_RATE_8K" value="8000"></el-option>
</el-select>
</el-form-item>
<el-form-item label="语调" prop="pitchrate">
<el-slider
v-model="dataForm.pitchrate"
show-input
:min="0" :max="500"
>
</el-slider>
</el-form-item>
<el-form-item label="语速" prop="speechrate">
<el-slider v-model="dataForm.speechrate" :min="0" :max="500"></el-slider>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
data () {
return {
isfeeling: false,
options: [{
label: '多情感',
options: [{
value: 'zhimi_emo',
label: '女生_多情感'
}, {
value: 'zhibei_emo',
label: '童声_多情感'
},
{
value: 'zhitian_emo',
label: '甜甜女生_多情感'
}
]
}, {
label: '仅支持中文',
options: [{
value: 'xiaobei',
label: '萝莉女音'
}, {
value: 'ailun',
label: '悬疑解说'
}, {
value: 'laotie',
label: '东北老铁'
}, {
value: 'laomei',
label: '吆喝女声'
}, {
value: 'xiaoze',
label: '湖南男音'
}, {
value: 'aikan',
label: '天津话男声'
}, {
value: 'chuangirl',
label: '四川话女声'
}, {
value: 'jiajia',
label: '粤语女声'
}]
},
{
label: '仅支持英文',
options: [{
value: 'abby',
label: '美音女声'
}, {
value: 'andy',
label: '美音男声'
}, {
value: 'eric',
label: '英音男声'
}, {
value: 'emily',
label: '英音女声'
}]
}],
visible: false,
dataForm: {
id: 0,
content: '',
name: '',
samplerate: '',
pitchrate: 50,
speechrate: 0,
emo: '',
intensity: '1.0'
},
dataRule: {
content: [
{ required: true, message: '文本不能为空', trigger: 'blur' }
],
name: [
{ required: true, message: '发音人不能为空', trigger: 'blur' }
],
samplerate: [
{ required: true, message: '采样频率不能为空', trigger: 'blur' }
],
pitchrate: [
{ required: true, message: '语调不能为空', trigger: 'blur' }
],
speechrate: [
{ required: true, message: '语速不能为空', trigger: 'blur' }
]
}
}
},
methods: {
isfeel () {
if (this.dataForm.name === 'zhimi_emo' || this.dataForm.name === 'zhibei_emo' || this.dataForm.name === 'zhitian_emo') {
this.isfeeling = true
} else {
this.isfeeling = false
this.this.dataForm.emo = ''
this.dataForm.intensity = '1.0'
}
},
init (id) {
this.dataForm.id = id || 0
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.id) {
this.$http({
url: this.$http.adornUrl(`/voice/voice/info/${this.dataForm.id}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 0) {
this.dataForm.content = data.voice.content
this.dataForm.name = data.voice.name
this.dataForm.samplerate = data.voice.samplerate
this.dataForm.pitchrate = data.voice.pitchrate
this.dataForm.speechrate = data.voice.speechrate
}
})
}
})
},
// 表单提交,用自己的方式提交
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(`/voice/voice/${!this.dataForm.id ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'id': this.dataForm.id || undefined,
'content': this.dataForm.content,
'name': this.dataForm.name,
'samplerate': this.dataForm.samplerate,
'pitchrate': this.dataForm.pitchrate,
'speechrate': this.dataForm.speechrate,
'emo': this.dataForm.emo,
'intensity': this.dataForm.intensity
})
}).then(({data}) => {
if (data && data.code === 0) {
this.isfeeling = false
this.dataForm.emo = ''
this.dataForm.intensity = '1.0'
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.visible = false
this.$emit('refreshDataList')
}
})
} else {
this.$message.error(data.msg)
}
})
}
})
}
}
}
</script>
3:后端
首先在配置文件中写好一些配置
voice:
adress: D:\\桌面\\voice\\ 自己存储生成的语音的位置,可以是本地或者远程
appKey: 自己的阿里云appKey
keyid: 自己的阿里云 keyid
secret: 自己的阿里云secret
然后建表

3.1Controller
这里用的mybatis-plus所以增删改查都比较简单,值得注意的增加(save)的时候需要生成mp3文件,大家只需要看增加部分就可以了,查看等操作根据自己的项目来
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import cn.hutool.core.date.DateUtil;
import com.alibaba.nls.client.AccessToken;
import com.alibaba.nls.client.protocol.NlsClient;
import io.renren.modules.voice.myenum.VoiceEnum;
import io.renren.modules.voice.voiceutils.MyClient;
import io.renren.modules.voice.voiceutils.SpeechSynthesizerutils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.renren.modules.voice.entity.VoiceEntity;
import io.renren.modules.voice.service.VoiceService;
import io.renren.common.utils.PageUtils;
import io.renren.common.utils.R;
import javax.xml.crypto.Data;
/**
* @author LMY
* @email 2269467209@qq.com
* @date 2022-08-20 15:19:36
*/
@RestController
@RequestMapping("voice/voice")
public class VoiceController {
@Autowired
private VoiceService voiceService;
@Value("${voice.adress}")
private String adress;
@Value("${voice.appKey}")
private String appKey;
@Autowired
private NlsClient client;
/**
* 列表
*/
@RequestMapping("/list")
@RequiresPermissions("voice:voice:list")
public R list(@RequestParam Map<String, Object> params) {
// 此处根据自己的项目来
PageUtils page = voiceService.queryPage(params);
return R.ok().put("page", page);
}
/**
* 保存
*/
@RequestMapping("/save")
@RequiresPermissions("voice:voice:save")
public R save(@RequestBody VoiceEntity voice) {
// 拿到声音的枚举,应为阿里云的声音对象都是chuangirl,为了前端显示方便我们需要在数据库中保存这样的四川话女声,看后文枚举类
VoiceEnum voiceEnum = VoiceEnum.getEnum(voice.getName());
//创建时间作为文件名的一部分
Date date = new Date();
String date1 = DateUtil.format(date, "yyyy-MM-dd-HH-mm-ss");
// 文件名
String adress1 = voiceEnum.getName() + date1 + ".mp3";
String newadress = adress + adress1;
// 生成语音生成工具,看后文
SpeechSynthesizerutils utils = new SpeechSynthesizerutils(appKey, client, voice);
utils.process(newadress);
voice.setAdress(adress1);
voice.setName(voiceEnum.getName());
voice.setCreatedate(date);
voiceService.save(voice);
// 返回给前端的值根据自己的项目来
return R.ok();
}
/**
* 删除
*/
@RequestMapping("/delete")
@RequiresPermissions("voice:voice:delete")
public R delete(@RequestBody Integer[] ids) {
// 此处根据自己的项目来
voiceService.removeByIds(Arrays.asList(ids));
return R.ok();
}
}
VoiceEnum
public enum VoiceEnum {
VOICE_ZHIMI("zhimi_emo","女生_多情感"),
VOICE_ZHIBEI("zhibei_emo","童声_多情感"),
VOICE_ZHITIAN("zhitian_emo","甜甜女生_多情感"),
VOICE_XIAOBEI("xiaobei","萝莉女音"),
VOICE_AILUN("ailun","悬疑解说"),
VOICE_LAOTIE("laotie","东北老铁"),
VOICE_LAOMI("laomei","吆喝女声"),
VOICE_XIAOZE("xiaoze","湖南男音"),
VOICE_AIKEN("aikan","天津话男声"),
VOICE_CHUANDIRL("chuangirl","四川话女声"),
VOICE_JIAJIA("jiajia","粤语女声"),
VOICE_ABBY("abby","美音女声"),
VOICE_ANDY("andy","美音男声"),
VOICE_ERIC("eric","英音男声"),
VOICE_eMILY("emily","英音女声");
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String code;
private String name;
VoiceEnum(String code, String name) {
this.code=code;
this.name=name;
}
// 根据拼音名字拿到汉字名字
public static VoiceEnum getEnum(String code){
for(VoiceEnum voice:VoiceEnum.values()){
if(voice.code.equals(code)){
return voice;
}
}
return null;
}
}
SpeechSynthesizerutils
package io.renren.modules.voice.voiceutils;
import com.alibaba.nls.client.AccessToken;
import com.alibaba.nls.client.protocol.NlsClient;
import com.alibaba.nls.client.protocol.OutputFormatEnum;
import com.alibaba.nls.client.protocol.SampleRateEnum;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizer;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerListener;
import com.alibaba.nls.client.protocol.tts.SpeechSynthesizerResponse;
import io.renren.modules.voice.entity.VoiceEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SpeechSynthesizerutils {
private static final Logger logger = LoggerFactory.getLogger(SpeechSynthesizerutils.class);
private static long startTime;
private String appKey;
private VoiceEntity voice;
NlsClient client;
public SpeechSynthesizerutils(String appKey,NlsClient client , VoiceEntity voice) {
// 调用的这一个
this.appKey = appKey;
this.voice=voice;
this.client=client;
}
public SpeechSynthesizerutils(String appKey, String accessKeyId, String accessKeySecret, String url) {
// 没有调用这个方法
this.appKey = appKey;
AccessToken accessToken = new AccessToken(accessKeyId, accessKeySecret);
try {
accessToken.apply();
System.out.println("get token: " + accessToken.getToken() + ", expire time: " + accessToken.getExpireTime());
if(url.isEmpty()) {
client = new NlsClient(accessToken.getToken());
}else {
client = new NlsClient(url, accessToken.getToken());
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static SpeechSynthesizerListener getSynthesizerListener(String adress) {
SpeechSynthesizerListener listener = null;
try {
listener = new SpeechSynthesizerListener() {
File f=new File(adress);
FileOutputStream fout = new FileOutputStream(f);
private boolean firstRecvBinary = true;
//语音合成结束
@Override
public void onComplete(SpeechSynthesizerResponse response) {
//调用onComplete时表示所有TTS数据已接收完成,因此为整个合成数据的延迟。该延迟可能较大,不一定满足实时场景。
System.out.println("name: " + response.getName() +
", status: " + response.getStatus()+
", output file :"+f.getAbsolutePath()
);
}
//语音合成的语音二进制数据
@Override
public void onMessage(ByteBuffer message) {
try {
if(firstRecvBinary) {
//计算首包语音流的延迟,收到第一包语音流时,即可以进行语音播放,以提升响应速度(特别是实时交互场景下)。
firstRecvBinary = false;
long now = System.currentTimeMillis();
logger.info("tts first latency : " + (now - SpeechSynthesizerutils.startTime) + " ms");
}
byte[] bytesArray = new byte[message.remaining()];
message.get(bytesArray, 0, bytesArray.length);
fout.write(bytesArray);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFail(SpeechSynthesizerResponse response){
//task_id是调用方和服务端通信的唯一标识,当遇到问题时需要提供task_id以便排查。
System.out.println(
"task_id: " + response.getTaskId() +
//状态码 20000000 表示识别成功
", status: " + response.getStatus() +
//错误信息
", status_text: " + response.getStatusText());
}
};
} catch (Exception e) {
e.printStackTrace();
}
return listener;
}
public void process(String adress) {
SpeechSynthesizer synthesizer = null;
try {
//创建实例,建立连接。
synthesizer = new SpeechSynthesizer(client, getSynthesizerListener(adress));
synthesizer.setAppKey(appKey);
//设置返回音频的编码格式
synthesizer.setFormat(OutputFormatEnum.MP3);
if(voice.getEmo().length()>1){
System.out.println(voice.getEmo().length());
String voicetext="<speak voice=\""+voice.getName()+"\">"+"<emotion category=\"happy\" intensity=\""+voice.getIntensity()+"\">"+voice.getContent()+"</emotion>"+"</speak>";
synthesizer.setText(voicetext);
System.out.println(voicetext);
}else{
//发音人
synthesizer.setVoice(voice.getName());
//设置用于语音合成的文本
synthesizer.setText(voice.getContent());
}
//设置返回音频的采样率
if(voice.getSamplerate()==16000){
synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K);
}else {
synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_8K);
}
//语调,范围是-500~500,可选,默认是0。
synthesizer.setPitchRate(voice.getPitchrate());
//语速,范围是-500~500,默认是0。
synthesizer.setSpeechRate(voice.getSpeechrate());
// 是否开启字幕功能(返回相应文本的时间戳),默认不开启,需要注意并非所有发音人都支持该参数。
synthesizer.addCustomedParam("enable_subtitle", false);
//此方法将以上参数设置序列化为JSON格式发送给服务端,并等待服务端确认。
long start = System.currentTimeMillis();
synthesizer.start();
logger.info("tts start latency " + (System.currentTimeMillis() - start) + " ms");
SpeechSynthesizerutils.startTime = System.currentTimeMillis();
//等待语音合成结束
synthesizer.waitForComplete();
logger.info("tts stop latency " + (System.currentTimeMillis() - start) + " ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭连接
if (null != synthesizer) {
synthesizer.close();
}
}
}
public void shutdown() {
client.shutdown();
}
}
需要注意的是工具类中有一个客户端client我们用的单例,应为如果每一个生成语音的请求都生成一个client,太浪费资源了,就是一个连接可以理解为和数据库连接一样,看我们是如何创建这个单例的
MyClient
@Configuration
public class MyClient {
@Value("${voice.keyid}")
private String keyid;
@Value("${voice.secret}")
private String secret;
NlsClient client;
// @Bean是一个方法级别上的注解,主要用在配置类中,将对象注入容器
@Bean
public NlsClient client() {
AccessToken accessToken = new AccessToken(keyid, secret);
//应用全局创建一个NlsClient实例,默认服务地址为阿里云线上服务地址。
//获取token,使用时注意在accessToken.getExpireTime()过期前再次获取。默认19天
try {
accessToken.apply();
System.out.println("get token: " + accessToken.getToken() + ", expire time: " + accessToken.getExpireTime());
client = new NlsClient(accessToken.getToken());
} catch (IOException e) {
e.printStackTrace();
}
return client;
}
}
数据库文件和源代码请看这里
源代码
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
更多推荐


所有评论(0)