原创 一枚少年郎 后台技术汇 2024年11月15日 20:09 广东
一、业务分层
二、前置工作
2.1 API密钥创建
可以从这里进入:https://console.cloud.tencent.com/cam/capi
2.2 免费额度申领
只要开通了通用语音合成服务(长文本暂不支持),无论选择预付费还是后付费的计费方式,都可以享受免费调用额度,免费调用额度将以免费资源包的形式配送,需要在 语音合成控制台 领取,领取成功后会在计费结算时优先扣减。
2.2.1 语音合成TTS免费额度
可以从这里进入:https://console.cloud.tencent.com/tts/resourcebundle
2.2.2 混元大模型免费额度
可以从这里进入:https://console.cloud.tencent.com/hunyuan/packages
三、开发工作
云数据库和云服务器,由于时间关系,就暂时用本地个人PC替代了。
3.1 业务时序图
3.2 云开发组件选型
1、腾讯云语音合成TTS
腾讯语音合成(TTS)能将文本转化为拟人化语音,满足多场景需求。具有高拟真度、灵活设置、声音多样等特性,支持多种语言、音色、音量、语速等。开通可免费领取三个月资源,接口有必填和选填参数,可通过调试选择,测试返回 base64 编码音频,前端操作简单。
产品特性
高拟真度
腾讯云基于业界领先技术构建的语音合成系统,具备合成速度快、合成语音自然流畅等特点,合成语音拟真度高,能够符合多样的应用场景,让设备和应用轻松发声,人机语音交互效果更加逼真。
灵活设置
腾讯云语音合成支持中文、英文、粤语、四川话,也可以合成中英混读语音;支持根据业务需求选择合适的音量、语速等属性;支持离线音频文件和实时音频流两种合成格式;支持电话、移动 App 等多种场景和合成效果选择。
声音多样
腾讯云语音合成支持多种男声、女声的选择,使得音色能够覆盖多样化的应用场景,适用于电话客服,小说朗读,消息播报等场景。此外,腾讯云支持为企业客户定制发声人。
付费模式
预付费
预付费资源包支持多种规格,有效期均为1年,1年内若资源包次数未使用完,则过期作废;若超额调用,则超额部分转入后付费模式。
后付费
通用语音合成 按实际使用量进行计费,所有计费服务的计费周期都是自然日,每日会对上一日用量输出账单并扣费。
长文本语音合成按实际使用量进行计费,所有计费服务的计费周期都是自然日,每日会对上一日用量输出账单并扣费。
<!--br {mso-data-placement:same-cell;}--> td {white-space:nowrap;border:1px solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
计费模式 | 预付费 | 后付费 |
---|---|---|
付款方式 | 预先付费 | 结算后付费 |
计费周期 | 年 | 通用语音合成(日)长文本语音合成(日) |
适用场景 | 适用于使用量稳定,或对一年内使用量有准确预估的业务 | 适用于使用量有较大波动性,或无法预估的业务 |
2、腾讯混元大模型
产品特性
腾讯混元大模型(Tencent Hunyuan)是由腾讯研发的大语言模型,具备强大的中文创作能力,复杂语境下的逻 辑推理能力,以及可靠的任务执行能力。
腾讯混元通过采用混合专家模型(MoE)结构,推动了性能提升和推理成本下降。在中文表现尤其是在文本生成、数理逻辑和多轮对话上性能表现卓越,整体处于业界领先水平。支持AI 搜索联网插件,通过整合腾讯优质的内容生态(如微信公众号、视频号等)和全网搜索,使混元具备强大的时新、深度内容获取和AI问答能力。
同时,混元还采用了各种技术手段来提高模型的性能和效果,例如使用掩码策略、使用不同的优化算法、进行数据增强等。这些技术手段可帮助模型更好地处理文本数据,提高模型的泛化能力和生成效果。
腾讯混元大模型目前覆盖四大核心能力:
多轮对话
具备上下文理解和长文记忆能力,流畅完成各专业领域的多轮问答。
知识增强
有效解决事实性、时效性问题,快速检索相关内容,提升内容生成效果。
逻辑推理
准确理解用户意图,擅长数学推导,基于输入数据或信息进行推理、分析和规划。
内容创作
支持文学创作、文本概要、角色扮演、文生图和图生文等多模态创作能力,生成内容流畅、规范、中立、客观。
付费模式
云数据库优势
腾讯云数据库 MySQL 为用户提供更轻松地云上设置、操作和扩展数据库服务,具备灵活易用、高可用、高数据安全可靠性等优势:
<!--br {mso-data-placement:same-cell;}--> td {white-space:nowrap;border:1px solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
对比项 | 云数据库 MySQL | CVM 自建 | 自购服务器搭建数据库 |
---|---|---|---|
可用性 | 双节点、三节点提供自研高可用系统,实现30秒内故障恢复。只读实例自动实现负载均衡。读写分离使用方便。未来会推出分析节点,满足分析型场景需求。 | 需要单独购买高可用系统。需要单独实现或者购买负载均衡服务。分析型场景需要与分析型数据库结合,搭建难度大、成本高。 | 单机实例,少则两小时,多则等待配货数周。需要单独购买高可用系统。需要单独实现或者购买负载均衡设备。分析型场景需要与分析型数据库结合,搭建难度大、成本高。 |
可靠性 | 数据可靠性高,自动主备复制、数据备份、日志备份等。MySQL 5.7三节点与 MySQL 8.0三节点,实现 RPO(Recovery Point Object)= 0,数据无丢失;RTO(Recovery Time Objective) 通常情况在1分钟左右。 | 在好的架构下才能实现高可靠性。实现 RPO = 0的成本极高,需要单独购买研发服务。 | 数据可靠性一般,取决于单块磁盘的损害概率。实现 RPO = 0的成本极高,需要单独购买研发服务。 |
易用性 | 云数据库 MySQL 的本地盘实例性能极佳。增加只读实例之后性能强劲且负载均衡。DBbrain 提供高级优化能力。性能分析满足大部分监控及性能优化数据库场景。 | CVM 本地盘意味着降低数据可靠性,采用云盘需要规划架构,成本支出较大。基于 SSD 的 CVM 自建 MySQL 性能低于基于 SSD 的云数据库 MySQL 性能。实现集群版的难度较高,咨询成本较高,维护成本极高。依赖资深 DBA,支出大,受制于人。 | 无自动备份系统,流式备份能力需要单独实现,实现按时间点恢复功能成本高。需要单独购买或配置监控系统,通道较少,成本较高。异地数据中心成本极高,技术实现难度也大,很难实现异地容灾。版本升级成本高。 |
云数据库MySQL 的性能优势让我们可以以更少的数据库数量支撑更高的业务并发请求量,简化了后端架构,使得整体IT 架构更易于管理和运维。
另外,腾讯云提供99.9996% 的数据可靠性和99.95% 的服务可用性,拥有完善的数据自动备份和无损恢复机制。
4、云服务器
可以从入口购买:https://buy.cloud.tencent.com/cvm?tab=lite&wanIp=0&templateCreateMode=createLt&isBackup=false&backupDiskType=ALL&backupDiskCustomizeValue=&backupQuotaSegment=1&backupQuota=1
5、前端框架
vue3项目创建
$ npm create vue@latest
$ cd <your-project-name>
$ npm install
$ npm run dev
页面开发
<template>
<div class="div">
<!-- 使用v-bind指令绑定图片的src属性 -->
<div>
<img :src="imageSrc" alt="Vue Logo" height="580" width="1000">
</div>
<div>
<input type="text" v-model="nameValue" placeholder="输入元素:祝福名字"/>
<input type="text" v-model="ageValue" placeholder="输入元素:对方年龄"/>
<input type="text" v-model="typeValue" placeholder="输入对方年龄段:1-宝宝,2-儿童,3-女孩,4-男孩,5-女士,6-男士"/>
<input type="text" v-model="extentInfoValue" placeholder="输入元素:生成文本要求"/>
<!-- 按钮,点击后发送请求 -->
<button @click="createAudio">生成音频</button>
</div>
<div>
<input type="text" v-model="inputValue" placeholder="输入语音id"/>
<!-- 按钮,点击后发送请求 -->
<button @click="fetchAudio">获取音频</button>
</div>
<!-- 音频播放器 -->
<div>
<audio ref="myAudio1" controls>
<source ref="audioSrc" type="audio/awv" />
Your browser does not support the audio element.
</audio>
</div>
</div>
</template>
<script lang="ts" setup name="App">
import { RouterView, RouterLink } from 'vue-router';
// RouterLink是组件,内置属性 active-class
import { ref } from 'vue';
import {axios} from 'axios';
const nameValue = ref('');
const ageValue = ref('');
const typeValue = ref('');
const extentInfoValue = ref('');
async function createAudio() {
try {
// 构建请求URL hunyuan
// const response2 = await fetch('/api/create_content_and_voice?' +
const response2 = await fetch('http://localhost:11444/hunyuan/create_content_and_voice?' +
'name='+encodeURIComponent(nameValue.value) + '&' +
'age='+ ageValue.value + '&' +
'type=' + typeValue.value + '&' +
'extendInfo=' + encodeURIComponent(extentInfoValue.value),
{
method: 'post',
headers: {
'Access-Control-Allow-Origin':'http://localhost:11444',
'Access-Control-Allow-Credentials': 'true'
}
}
);
console.log("post response, ", response2);
} catch (error) {
console.error('Error fetching audio:', error);
}
}
const myAudio1 = ref(null);
const audioSrc = ref(null);
const dataString = ref('');
const inputValue = ref('');
const imageSrc = ref('src/asserts/pic/girl.png');
async function fetchAudio() {
try {
// 构建请求URL
const url = 'http://localhost:11444/hunyuan/get_voice_by_id?id='+inputValue.value;
console.log("inputValue, ", inputValue.value);
// 发送GET请求
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 假设后端返回的是音频文件的URL
console.log('response => ', response);
const readableStream = response.body;
if (readableStream) {
dataString.value = await streamToString(readableStream);
}
// 更新audioSrc的值
myAudio1.value.src = 'data:audio/wav;base64,' + dataString.value;
} catch (error) {
console.error('Error fetching audio:', error);
}
}
// 反序列化语音数据
async function streamToString(readableStream) {
const reader = readableStream.getReader();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += new TextDecoder().decode(value);
}
return result;
}
</script>
页面效果
6、后端框架
SpringBoot项目创建,并添加pom依赖即可。
1)pom主要依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>hunyuan-tts-integration</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 整合mybatis相关依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- 数据库驱动(例如 MySQL) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.1139</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2)application.properties配置文件
spring.application.name=hunyuan-tts-integration
## 配置腾讯混元模型参数
tencent.hunyuan.secret-id: xxxxx
tencent.hunyuan.secret-key: xxxxx
# JDBC配置数据库名称(默认走的是配置中心的users-dev/users-test.properties)
spring.datasource.url = jdbc:mysql://localhost:3306/mac_mini?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=12345678
spring.datasource.name=mac_mini
## mybatis依赖
#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml
对应配置类
@Configuration
@ConfigurationProperties(prefix = "tencent.hunyuan")
@Data
public class TecentHunyuanConfig {
private String secretId;
private String secretKey;
}
TTS客户端和大模型客户端初始化
@Configuration
public class TencentHunyuanInit {
@Autowired
private TecentHunyuanConfig config;
@Bean
public HunyuanClient hunyuanClient() {
Credential credential = new Credential(config.getSecretId(), config.getSecretKey());
return new HunyuanClient(credential, "ap-guangzhou");
}
@Bean
public TtsClient ttsClient() {
Credential credential = new Credential(config.getSecretId(), config.getSecretKey());
return new TtsClient(credential, "ap-guangzhou");
}
}
3)MVC配置-请求跨域配置
/**
* 解决前端跨域问题
*/
@Configuration
@EnableAspectJAutoProxy
public class WebAppConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/*/**")
// Access-Control-Allow-Origin 允许跨域的域名,如果携带cookies则不能为“*”
.allowedOrigins("*")
// Access-Control-Allow-Methods
.allowedMethods("GET","POST","PUT", "OPTIONS", "DELETE");
}
}
4)Controller接口层
package com.bryant.controller;
import com.bryant.model.TtsVoice;
import com.bryant.service.HunyuanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
@RequestMapping("/hunyuan")
@RestController
public class HunyuanController {
@Autowired
private HunyuanService hunyuanService;
@PostMapping("/create_content_and_voice")
public Long createContentAndVoice(
@RequestParam(value = "name") String name,
@RequestParam(value = "age") Integer age,
@RequestParam(value = "type") String type,
@RequestParam(value = "extendInfo") String extendInfo
) {
TtsVoice ttsvo = hunyuanService.createContentAndVoice(name, age, type, extendInfo);
return ttsvo.getId();
}
@GetMapping("/get_voice_by_id")
public String getVoiceById(@RequestParam("id") String id) {
TtsVoice voice = hunyuanService.getVoiceById(Long.valueOf(id));
if (Objects.isNull(voice)) {
return "null";
}
return voice.getVoice();
}
}
5)service层
package com.bryant.service;
import com.bryant.mapper.TtsVoiceMapper;
import com.bryant.model.TtsVoice;
import com.bryant.service.util.HunyuanClient;
import com.bryant.service.util.ObjectTypeEnum;
import com.bryant.service.util.TtsClient;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.tencentcloudapi.common.SSEResponseModel;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.hunyuan.v20230901.models.ChatCompletionsRequest;
import com.tencentcloudapi.hunyuan.v20230901.models.ChatCompletionsResponse;
import com.tencentcloudapi.hunyuan.v20230901.models.Choice;
import com.tencentcloudapi.hunyuan.v20230901.models.Message;
import com.tencentcloudapi.tts.v20190823.models.TextToVoiceRequest;
import com.tencentcloudapi.tts.v20190823.models.TextToVoiceResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.UUID;
@Service
@Slf4j
public class HunyuanService {
private final static String PROMOTE_TEMPLATE = "今天是 %s %s 的 %s 岁生日,希望你能帮忙写一段200字左右的祝福语,要求是 %s,输出内容限制在120字以内";
@Autowired
private HunyuanClient client;
@Autowired
private TtsClient ttsClient;
@Autowired
private TtsVoiceMapper ttsVoiceMapper;
public TtsVoice createContentAndVoice(String babyName, Integer age, String type, String extendInfo) {
StopWatch watch = new StopWatch("createContentAndVoice");
watch.start("getContent");
// 1、ai文生文
String content = this.getContent(babyName, age, type, extendInfo);
watch.stop();
watch.start("getTTs");
// 2、语音合成
String tts = this.getTTs(content);
watch.stop();
// 3、保存到数据库
watch.start("insert tts");
TtsVoice ttsVoice = buildTtsVoice(content, type, age, babyName, tts);
ttsVoiceMapper.insert(ttsVoice);
watch.stop();
log.info("createContentAndVoice, {}", watch);
return ttsVoice;
}
private TtsVoice buildTtsVoice(String content, String type, Integer age, String babyName, String tts) {
TtsVoice ttsVoice = new TtsVoice();
ttsVoice.setOriginText(content);
ttsVoice.setVoice(tts);
ttsVoice.setAge(age);
ttsVoice.setType(Integer.valueOf(ObjectTypeEnum.fromCode(type).getValue()));
ttsVoice.setPath(babyName);
ttsVoice.setCreatedAt(new Date());
return ttsVoice;
}
public TtsVoice getVoiceById(Long id) {
TtsVoice ttsVoice = ttsVoiceMapper.getById(id);
return ttsVoice;
}
public String getContent(String name, Integer age, String type, String extendInfo) {
ChatCompletionsRequest req = new ChatCompletionsRequest();
String content = String.format(PROMOTE_TEMPLATE, name, ObjectTypeEnum.fromCode(type).getChName(), age, extendInfo);
Message[] msgs = new Message[1];
Message msg = new Message();
msg.setContent(content);
msgs[0] = msg;
req.setMessages(msgs);
// hunyuan ChatCompletions 同时支持 stream 和非 stream 的情况
req.setStream(true);
msg.setRole("user");
req.setModel("hunyuan-standard");
try {
ChatCompletionsResponse resp = client.ChatCompletions(req);
StringBuilder stringBuilder = new StringBuilder();
if (req.getStream()) {
// stream 示例
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
for (SSEResponseModel.SSE e : resp) {
ChatCompletionsResponse eventModel = gson.fromJson(e.Data, ChatCompletionsResponse.class);
Choice[] choices = eventModel.getChoices();
if (choices.length > 0) {
log.info("choices[0].getDelta().getContent() = {}", choices[0].getDelta().getContent());
stringBuilder.append(choices[0].getDelta().getContent());
}
// 如果希望在任意时刻中止事件流, 使用 resp.close() + break
boolean iWantToCancelNow = false;
if (iWantToCancelNow) {
resp.close();
break;
}
}
return stringBuilder.toString();
} else {
// 非 stream 示例
// 通过 Stream=false 参数来指定非 stream 协议, 一次性拿到结果
System.out.println(resp.getChoices()[0].getMessage().getContent());
}
} catch (TencentCloudSDKException e) {
log.info("TencentCloudSDKException, {}", e);
}
return "error";
}
public String getTTs(String text) {
TextToVoiceRequest request = new TextToVoiceRequest();
try {
request.setText(text);
request.setSkipSign(true);
request.setModelType(5l);
request.setVoiceType(301022l);
request.setVolume(1f);
request.setSpeed(1f);
request.setCodec("wav");
request.setSessionId(UUID.randomUUID().toString());
request.setEmotionCategory("happy");
request.setEmotionIntensity(150L);
TextToVoiceResponse resp = ttsClient.TextToVoice(request);
try {
// TextToVoiceResponse.toJsonString(resp);
// 存储的是base64解码后的数据
// byte[] audioBytes = Base64.getDecoder().decode(resp.getAudio());
// String filePath = "olllama-integration/src/main/resources/voice/tts.wav";
// FileOutputStream fos = new FileOutputStream(filePath);
// fos.write(audioBytes);
// 存储的是base64解码前的数据
String filePath2 = "olllama-integration/src/main/resources/voice/tts2.wav";
FileOutputStream fos2 = new FileOutputStream(filePath2);
fos2.write(resp.getAudio().getBytes());
fos2.close();
return resp.getAudio();
} catch (IOException e) {
log.info("ex, ", e);
}
} catch (TencentCloudSDKException e) {
throw new RuntimeException(e);
}
return "fail";
}
}
6)Mapper层
接口
@Repository
public interface TtsVoiceMapper {
void insert(TtsVoice ttsVoice);
TtsVoice getById(@Param("id") Long id);
}
xml映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bryant.mapper.TtsVoiceMapper">
<resultMap id="BaseResultMap" type="com.bryant.model.TtsVoice">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="type" property="type" javaType="INTEGER"></result>
<result column="age" property="age" javaType="INTEGER"></result>
<result column="path" property="path" javaType="String"></result>
<result column="voice" property="voice" javaType="String"></result>
<result column="origin_text" property="originText" javaType="String"></result>
<result column="created_at" property="createdAt" javaType="Date"></result>
</resultMap>
<sql id="base_columns">
`id`, `type`, age, path, voice, origin_text, created_at
</sql>
<insert id="insert" parameterType="map">
insert into music_path
(id, type, age, path, voice, origin_text, created_at)
values
(#{id}, #{type}, #{age}, #{path}, #{voice}, #{originText}, #{createdAt})
</insert>
<select id="getById" parameterType="map" resultMap="BaseResultMap">
select
<include refid="base_columns"/>
from music_path
where id = #{id}
</select>
</mapper>
四、效果演示
4.1 输入祝福语元素
我们就给马总输入一段“严肃又搞怪的”生日祝福语吧。
4.2 数据库确实保存了AI生成的数据
马化腾先生,祝您25岁生日快乐!
在这个特别的日子里,我们以一种既严肃又搞怪的方式为您送上祝福。
愿您的智慧如同宇宙的星辰,不断闪耀;
愿您的事业如同互联网的浪潮,汹涌澎湃。
同时,也别忘了放松一下,偶尔展现您搞怪的一面,让生活更加多彩。
毕竟,连太阳都要休息,您也要照顾好自己哦!
愿您的未来充满无限可能,25岁的您更加精彩!
4.3 获取语音音频
4.4 拓展方向
我们在通过其他工具,将音频和图片生成一个视频,就更好了!