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

华为云语音交互SIS的使用案例(文字转语音-详细教程)

文章目录

  • 题记
  • 一 、语音交互服务(Speech Interaction Service,简称SIS)
  • 二、功能介绍
    • 1、实时语音识别
    • 2、一句话识别
    • 3、录音文件识别
    • 4、语音合成
  • 三、约束与限制
  • 四、使用
    • 1、API
    • 2、SDK
  • 五、项目集成
    • 1、引入pom依赖
    • 2、初始化 Client
      • 1)准备参数
      • 2)nacos配置
      • 3)配置类-CommonClientsProperties.java
      • 4)初始化客户端配置-CommonClientsCache.java
      • 5)抽取公共文件客户端封装对象- CommonClientBean.java
      • 6)华为云语音生成客户端封装-HuaweiClientBean.java
      • 7)工具类-FileUtils.java
      • 8)封装公共请求参数-FileVoiceUploadReqDTO.java
      • 9)业务类调用-ArticleManageController.java

题记

本文将根据一种具体业务场景:语音播报(将一篇ai撰写的文章异步转换成语音文件进行播报)为案例演示华为云语音交互SIS的集成使用。

一 、语音交互服务(Speech Interaction Service,简称SIS)

语音交互服务(Speech Interaction Service,简称SIS)是一种人机交互方式,用户通过实时访问和调用API(Application Programming Interface,应用程序编程接口)将语音识别成文字或者将文本转换成逼真的语音等。

常用的应用场景参看官网:应用场景

二、功能介绍

Tip:根据你的需求场景,是否实时、大小、时长、是语音转文字,还是文字转语音等等评估应该使用下边哪种功能。

1、实时语音识别

实时语音识别服务,用户通过实时访问和调用API获取实时语音识别结果,支持的语言包含中文普通话、方言和英语,方言当前支持四川话、粤语和上海话。

  • 文本时间戳
    为音频转换结果生成特定的时间戳,从而通过搜索文本即可快速找到对应的原始音频。

  • 智能断句
    通过提取上下文相关语义特征,并结合语音特征,智能划分断句及添加标点符号,提升输出文本的可阅读性。

  • 中英文混合识别
    支持在中文句子识别中夹带英文字母、数字等,从而实现中、英文以及数字的混合识别。

  • 即时输出识别结果
    连续识别语音流内容,即时输出结果,并可根据上下文语言模型自动校正。

  • 自动静音检测
    对输入语音流进行静音检测,识别效率和准确率更高。

2、一句话识别

可以实现1分钟以内音频到文字的转换。对于用户上传的二进制音频格式数据,系统经过处理,生成语音对应的文字,支持的语言包含中文普通话、方言以及英语。方言当前支持四川话、粤语和上海话。

3、录音文件识别

对于录制的长语音进行识别,转写成文字,提供不同领域模型,具备良好的可扩展性,支持热词定制。

4、语音合成

文本转成语音,语音合成支持多种音色,可调节语调,语速,音量。

这里我将使用【4、语音合成】功能实现开篇提到的文章转语音播报的目的。

三、约束与限制

明确了要使用的功能,接下来看有哪些约束限制,是否与需求契合。使用【语音合成】功能的注意点:

  • 支持“华北-北京四”、“华东-上海一”区域。
  • 支持中文、英文、中英文,文本不长于500个字符
  • 支持合成采样率8kHz、16kHz。
    在这里插入图片描述

Tip:由上可知,如果文本大于500字符就需要切割再合并问题。

以上了解了需求场景能不能使用,接下来就看怎么用啦~

四、使用

主要有两种接入方式:APISDK

1、API

SIS服务提供了两种接口,包含REST(Representational State Transfer)API,支持您通过HTTPS请求调用。也包含WebSocket接口,支持Websocket协议。参看:API文档

本文使用SDK方式接入,API方式不过多赘述,可参考文档使用。

2、SDK

最新的sdk目前是3.1.128版本。

在这里插入图片描述
注意该SDK暂不支持websocket方法。

如果需要使用实时语音识别,可考虑使用替代SDK,当前支持Java SDK、Python SDK、CPP SDK、iOS SDK、Android SDK。

这里我不需要实时的,可以直接使用上边的最新sdk的方式。

五、项目集成

由于我的项目本身有华为云其他产品,为了兼容使用了3.1.116版本,以及排除了一些依赖。

1、引入pom依赖

 <dependency><groupId>com.huaweicloud.sdk</groupId><artifactId>huaweicloud-sdk-sis</artifactId><version>3.1.116</version><exclusions><exclusion><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId></exclusion></exclusions></dependency>

2、初始化 Client

注意:官方文档上显示的客户端client可能是未更新的或者和你本地引入的依赖里的客户端不匹配,根据实际情况使用你依赖里的客户端去处理就好,以及封装的请求对象。
【我这里依赖里的客户端是:SisClient,请求类:RunTtsRequest】

1)准备参数

首先需要一些认证信息、配置信息,可参考官网获取方式:在这里插入图片描述
请求参数:

在这里插入图片描述

目前SDK仅支持AK/SK认证方式。

2)nacos配置

我们将上边的信息以及可以调整的参数统一提取出来配置化,避免硬编码,这里我统一放到nacos中配置。

nacos配置文件内容:

#支持多租户分桶的文件服务配置,目前支持阿里云oss、亚马逊s3、华为云obs、NAS网络存储、微软云blob。
common:clients:#文件权限范围; default:平台, 租户code eg:100001- bucketOwner: default#桶类型; public:公有, private:私有; 其他自定义只作为备用桶, 需以_public或_private结尾bucketType: public#存储云类型; cloudType: huaweiyun#桶名称bucketName: obs-group-test-xxxxx#oss提供的内网访问域名 endpoint: https://obs.cn-north-4.myhuaweicloud.comaccessKeyId: YL6BxxxxxxxxxxxxxxxKLaccessKeySecret: w0pTVxxxxxxxxxxxxxx1hXnHprojectId: 0744xxxxxxxxxxxxxd9aregion: cn-north-4default:#默认的私有桶url有效时间,单位:秒。expiration: 3600#租户备用桶设置(只支持读取)buckets:#租户code- tenantCode: test#{bucketOwner}_{bucketType},根据bucketOwner和bucketType映射到上面配置的桶spareBucket: test_public#华为云语音合成音色设置    
sis-client:#语音格式头:wav、mp3、pcm 默认:wavaudioFormat: wav#采样率:16000、8000赫兹 默认:8000sampleRate: 8000#语音合成特征字符串property: chinese_huaxiaodong_common#语速speed: 0#音高pitch: 43#音量默认50volume: 44

3)配置类-CommonClientsProperties.java

CommonClientsProperties.java

@ConfigurationProperties(prefix = "common")
public class CommonClientsProperties {private List<Properties> clients = new ArrayList<>();public List<Properties> getClients() {return clients;}public void setClients(List<Properties> clients) {this.clients = clients;}@Datapublic static class Properties {private String bucketOwner;private String bucketType;private String cloudType;private String bucketName;private String endpoint;private String accessKeyId;private String accessKeySecret;private String region;private Integer expiration;private String baseDir;private String connectStr;private String projectId;}
}

4)初始化客户端配置-CommonClientsCache.java

这里可以做的通用一些,将每个平台自家的产品的客户端都单独封装在一起,比如华为云的obs、语音、视频等封装成华为云的客户端;阿里的oss、语音等等封装成阿里的客户端;统一给外层调用。

另外accessKey可能涉及到加解密等注意处理即可。
这里我们将生成的语音文件上传到华为云obs,所以一并将obs客户端、http的也初始化了。

/*** 文件客户端初始化*/
@Slf4j
public class CommonClientsCache {@ResourceCommonClientsProperties commonClientsProperties;private final Map<String, CommonClientBean> cache = new HashMap<>();@PostConstructpublic void init() {List<CommonClientsProperties.Properties> clientParams = commonClientsProperties.getClients();clientParams.forEach(properties -> {String key = String.format("%s_%s", properties.getBucketOwner(), properties.getBucketType());cache.put(key, buildCommonClientBean(properties));});}private CommonClientBean buildCommonClientBean(CommonClientsProperties.Properties properties) {String endpoint = properties.getEndpoint();String accessKeySecret = decode(properties.getAccessKeySecret());String bucketName = properties.getBucketName();CloudTypeEnum cloudType = CloudTypeEnum.valueOfType(properties.getCloudType());if (StringUtils.isBlank(bucketName) && StringUtils.isBlank(properties.getConnectStr())) {log.info("file client configuration missing");return null;}try {log.info("file client init start, endpoint:{},bucketName:{}", endpoint, bucketName);switch (Objects.requireNonNull(cloudType)) {case HUAWEIYUN:return getHuaWeiClientBean(properties, accessKeySecret);default:throw new FileBizException("cloud type is error");}} catch (Exception e) {log.error("file client init failed", e);return null;}}private String decode(String accessKey) {// 使用加密AK秘钥try {if (StringUtils.isNotEmpty(accessKey) && accessKey.contains(CoreConstants.ZAEC)) {accessKey = Zaenc.decryptData(accessKey);}} catch (Exception e) {log.error(" access key decrypt fail", e);}return accessKey;}private CommonClientBean getHuaWeiClientBean(CommonClientsProperties.Properties properties, String accessKeySecret) {ObsClient obsClient = new ObsClient(properties.getAccessKeyId(), accessKeySecret, properties.getEndpoint());HttpConfig httpConfig = HttpConfig.getDefaultHttpConfig().withIgnoreSSLVerification(true).withTimeout(10);ICredential auth = new BasicCredentials().withAk(properties.getAccessKeyId()).withSk(accessKeySecret).withProjectId(properties.getProjectId());SisClient sisClient = SisClient.newBuilder().withCredential(auth).withHttpConfig(httpConfig).withRegion(SisRegion.valueOf(properties.getRegion())).build();OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(180, TimeUnit.SECONDS).readTimeout(180, TimeUnit.SECONDS).writeTimeout(180, TimeUnit.SECONDS).build();return new HuaweiSisClientBean(properties, obsClient, sisClient,okHttpClient);}
}public S3ClientBean getClientByOwnerAndType(BucketOwnerEnum bucketOwner, BucketTypeEnum bucketType, String tenantCode) {String owner = bucketOwner.equals(BucketOwnerEnum.DEFAULT) ? bucketOwner.getType() : tenantCode;String key = String.format("%s_%s", owner, bucketType.getType());CommonClientBean s3Client = cache.get(key);//如果找不到租户桶,取公共桶if (s3Client == null && bucketOwner.equals(BucketOwnerEnum.TENANT)) {String defaultKey = String.format("%s_%s", BucketOwnerEnum.DEFAULT.getType(), bucketType.getType());s3Client = cache.get(defaultKey);}if (s3Client == null) {log.error("file client not found, bucketOwner:{}, bucketType:{}", bucketOwner, bucketType);throw new FileBizException("file client not found");}return s3Client;}

5)抽取公共文件客户端封装对象- CommonClientBean.java

不同的客户端各自实现,比如阿里、华为、亚马逊。

CommonClientBean.java


/*** 文件客户端封装对象**/
public interface CommonClientBean {/*** 云存储类型** @return CloudTypeEnum*/CloudTypeEnum getCloudType();/*** 基本目录** @return 基本目录*/String getBaseDir();/*** 上传文件** @param file 文件* @param key  文件保存路径*/void uploadMultipartFile(MultipartFile file, String key);/*** 上传文件* @param file 文件* @param key 文件Key* @return 文件Key*/default String uploadMultipartFileWithReturn(MultipartFile file, String key) {uploadMultipartFile(file, key);return key;}/*** 上传字节数组** @param bytes 字节数组* @param key   文件保存路径*/void uploadByteArray(byte[] bytes, String key);/*** 上传网络流** @param url 网络流地址* @param key 文件保存路径*/void uploadNetworkFlow(String url, String key);/*** 上传输入流** @param inputStream 輸入流* @param key         文件保存路径*/void uploadInputStream(InputStream inputStream, String key);/*** 追加上传** @param input    文件流* @param key      文件保存路径* @param position 追加位置*/void appendUpload(InputStream input, String key, Long position);/*** 根据Key获取文件下载流** @param key 文件key* @return 文件下载对象*/FileDownloadDTO downloadStream(String key);/*** 根据Key获取图片压缩url** @param key  文件key* @param size 文件大小* @return 图片压缩url*/String getCompressUrl(String key, int size);/*** 根据Key获取文件Url** @param key 文件key* @return 文件Url*/String getUrl(String key);/*** 根据Key获取文件Url* @param key 文件key* @param assetId 资产id* @return 文件Url*/default String getUrl(String key,String assetId){return getUrl(key);}/*** 根据Key获取文件大小** @param key 文件key* @return 文件大小*/Long getObjectLength(String key);/*** 发布视频* @param assetId 资产id*/default void publishVideo(String assetId) {//什么也不做}/*** CDN预热* @param assetId 资产id*/default void videoPreheat(String assetId) {//什么也不做}default String obs2vod(String fileName,String obsUrl) {//什么也不做return null;}default CredentialDTO securityToken(){//什么也不做return null;}default TemporarySignatureDTO createTemporarySignature(String objectKey){return null;}default byte[] convertTextToSpeech(FileVoiceUploadReqDTO dto) {return null;}
}

6)华为云语音生成客户端封装-HuaweiClientBean.java

新建个华为云的bean实现上边提到的common bean接口,进行扩展。

HuaweiClientBean.java


/*** 华为云语音交互服务客户端封装对象@Slf4j
@Getter
@SuppressWarnings("unchecked")
public class HuaweiClientBean implements CommonClientBean {/*** 桶名称*/private String bucketName;/*** 桶类型*/private BucketTypeEnum bucketType;/*** 云存储类型*/private CloudTypeEnum cloudType;/*** endpoint*/private String endpoint;/*** 自定义绑定域名*/private String bindingDomain;/*** 私有url有效期 单位:秒*/private Integer expiration;/*** 基本目录*/private String baseDir;/*** obs连接客户端*/private ObsClient s3Client;/*** 引入 sis 客户端*/private SisClient sisClient;/*** 引入 http client*/private OkHttpClient httpClient;private CommonClientsProperties.Properties properties;public HuaweiClientBean(CommonClientsProperties.Properties properties, ObsClient s3Client) {this.bucketName = properties.getBucketName();this.bucketType = BucketTypeEnum.valueOfTypeEndsWhit(properties.getBucketType());this.cloudType = CloudTypeEnum.valueOfType(properties.getCloudType());this.endpoint = UrlUtils.delProtocol(properties.getEndpoint());this.bindingDomain = UrlUtils.delProtocol(properties.getBindingDomain());this.expiration = properties.getExpiration();this.baseDir = properties.getBaseDir();this.s3Client = s3Client;}private final static String MATCHES = ".*[a-zA-Z\\d\\u4e00-\\u9fa5].*";/*** 最大字符长度*/public static Integer MAX_FILE_SIZE = 500;/*** 语音格式头:wav、mp3、pcm*/public static final List<String> VOICE_FORMATS = Arrays.asList("wav", "mp3", "pcm");/*** 采样率,支持“8000”、“16000”*/public static final List<String> SAMPLE_RATE_FORMATS = Arrays.asList("8000", "16000");/*** 文本转语音文件 - 上传到 SIS入口** @param*/public byte[] convertTextToSpeech(FileVoiceUploadReqDTO dto) {log.info(" start convertTextToSpeech :{}", JSONUtil.toJsonStr(dto));if (!ObjectUtil.isEmpty(dto) && !StringUtils.isEmpty(dto.getText())) {TtsConfig paramConfig = new TtsConfig();paramConfig.setSpeed(dto.getSpeed());paramConfig.setVolume(dto.getVolume());paramConfig.setPitch(dto.getPitch());paramConfig.setAudioFormat(TtsConfig.AudioFormatEnum.fromValue(dto.getAudioFormat()));//采样率,支持“8000”、“16000”,默认“8000”paramConfig.setSampleRate(TtsConfig.SampleRateEnum.fromValue(dto.getSampleRate()));paramConfig.setProperty(TtsConfig.PropertyEnum.fromValue(dto.getProperty()));//文本小于500个字符直接转换,如果大于500分段if (dto.getText().length() < MAX_FILE_SIZE) {return uploadTextToSis(dto.getText(), paramConfig);} else {return uploadTextToSisPart(dto.getText(), paramConfig);}}return null;}/*** 分段处理text** @param text* @param paramConfig* @return*/private byte[] uploadTextToSisPart(String text, TtsConfig paramConfig) {int length = text.length();int batchNum = (length % MAX_FILE_SIZE > 0) ? (length / MAX_FILE_SIZE + 1) : (length / MAX_FILE_SIZE);log.info("待处理数据总数:{},总批次数:{}", length, batchNum);int startIndex = 0;int endIndex = 0;Map map = new HashMap();List list = new ArrayList();if (batchNum > 0) {//循环批次数,计算待处理数据下标for (int currentNum = 1; currentNum <= batchNum; currentNum++) {//每次计算要处理的数据起始位置 终止位置String currentText = "";startIndex = (currentNum - 1) * MAX_FILE_SIZE;//最后一个批次特殊处理if (currentNum == batchNum) {endIndex = length ;} else {endIndex = startIndex + MAX_FILE_SIZE;}currentText = text.substring(startIndex, endIndex);//发送请求if(currentText.matches(MATCHES)){byte[] result = uploadTextToSis(currentText, paramConfig);list.add(result);}}// 合并字节数组return mergeByteArrays(list);}return null;}/*** 合并字节数组** @param byteArrayList* @return*/public byte[] mergeByteArrays(List<byte[]> byteArrayList) {// 计算所有字节数组的总长度int totalLength = 0;for (byte[] array : byteArrayList) {totalLength += array.length;}// 创建一个新的字节数组以存放合并结果byte[] mergedArray = new byte[totalLength];int currentIndex = 0;// 将每个字节数组复制到合并数组中for (byte[] array : byteArrayList) {System.arraycopy(array, 0, mergedArray, currentIndex, array.length);currentIndex += array.length;}return mergedArray;}/*** 发送请求并获取响应:合成后生成的语音数据,以Base64编码格式返回,并解码成byte数组** @param text* @param paramConfig* @return*/private byte[] uploadTextToSis(String text, TtsConfig paramConfig) {String data = uploadAssert(text, paramConfig);if (!ObjectUtil.isEmpty(data)) {return Base64.decodeBase64(data);}return null;}private String uploadAssert(String text, TtsConfig paramConfig) {// 构建请求对象RunTtsRequest request = new RunTtsRequest();TtsConfig configBody = new TtsConfig();//语音格式头:wav、mp3、pcm 默认:wavconfigBody.setAudioFormat(paramConfig.getAudioFormat());//采样率,支持“8000”、“16000”,默认“8000”configBody.setSampleRate(paramConfig.getSampleRate());//语速取值范围:-500~500 默认值:0configBody.setSpeed(paramConfig.getSpeed());//音高 取值范围: -500~500 默认值:0configBody.setPitch(paramConfig.getPitch());//音量 取值范围:0~100 默认值:50configBody.setVolume(paramConfig.getVolume());//语音合成特征字符串,组成形式为{language}_{speaker}_{domain},即“语种_人员标识_领域”configBody.setProperty(paramConfig.getProperty());PostCustomTTSReq body = new PostCustomTTSReq();body.withConfig(configBody);body.withText(text);request.withBody(body);log.info("uploadAssert start:{}", JSONUtil.toJsonStr(request));try {//发送请求并处理响应RunTtsResponse response = sisClient.runTts(request);if (!ObjectUtil.isEmpty(response.getResult())) {log.info("upload text to speech  success!");return response.getResult().getData();} else {log.error("upload text to speech  error, response:{}", response);return null;}} catch (Exception e) {log.error("upload text to speech  fail, text:{}", text, e);throw new FileBizException("upload vod multipart file fail");}}/*** 上传MultipartFile*/@Overridepublic void uploadMultipartFile(MultipartFile file, String key) {try {uploadInputStream(file.getInputStream(), key);} catch (IOException e) {log.error("upload obs multipart file fail, bucketName:{}", bucketName, e);throw new FileBizException("upload obs multipart file fail");}}/*** 上传输入流*/@Overridepublic void uploadInputStream(InputStream inputStream, String key) {try {PutObjectRequest request = new PutObjectRequest();request.setBucketName(bucketName);request.setObjectKey(key);request.setInput(inputStream);// 设置对象访问权限为公共读if (BucketTypeEnum.PUBLIC.equals(bucketType)) {request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);}s3Client.putObject(request);} catch (Exception e) {log.error("upload obs fail, bucketName:{}", bucketName, e);throw new FileBizException("upload obs fail");} finally {IoUtil.close(inputStream);}}
}

合成后生成的语音数据,以Base64编码格式返回。
如需生成音频,需要将Base64编码解码成byte数组,再保存为wav音频。

所以这里,当字符长度大于500,切割发送,再将返回的byte数组合并成生成完整的一个音频,再对视频进行业务处理,这里我选择将视频上传华为云obs存储,返回url供前端播放。

7)工具类-FileUtils.java

将封装好的客户端对外提供访问入口,可以封装成工具类等供server等调用

FileUtils.java


@Slf4j
public class FileUtils {private static CommonClientBean commonClientBean;private static CommonClientsCache commonClientsCache;private static HuaweiClientBean huaweiClientBean;/*** 上传text转成语音并上传obs*/public static FileUploadResDTO convertToSpeechAndUploadObs(FileVoiceUploadReqDTO dto) {//1.上传text转成byte[]FileUtils.initClient(BucketOwnerEnum.DEFAULT, BucketTypeEnum.PUBLIC, null);byte[] bytes = commonClientBean.convertTextToSpeech(dto);if (bytes == null || bytes.length == 0) {throw new FileBizException("file bytes cannot be empty");}try {//2.byte[]转语音文件AudioInputStream combinedAudioInputStream = new AudioInputStream(new ByteArrayInputStream(bytes),getAudioFormat(bytes),bytes.length);// 输出合并后的音频文件File hbFile = new File(dto.getPath());AudioSystem.write(combinedAudioInputStream, AudioFileFormat.Type.WAVE, hbFile);//3.上传语音文件到obsFileItem fileItem = createFileItem(dto.getPath(), dto.getFilename());String key = initClientAndGetKey(dto, UUID.randomUUID().toString());commonClientBean.uploadMultipartFile(new CommonsMultipartFile(fileItem), key);String url = commonClientBean.getUrl(key);// 最后删除临时文件释放资源if (hbFile.exists()) {hbFile.delete();}return new FileUploadResDTO(key, url, url, dto.getFilename());} catch (Exception e) {log.error("byte[]转语音文件异常", e);throw new FileBizException("byte convert speech file fail");}}private static void initClient(BucketOwnerEnum bucketOwner, BucketTypeEnum bucketType, String tenantCode) {commonClientsCache= commonClientsCache.getClientByOwnerAndType(bucketOwner, bucketType, tenantCode);}public static AudioFormat getAudioFormat(byte[] audioBytes) throws IOException, UnsupportedAudioFileException {ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(audioBytes);AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(byteArrayInputStream);return audioInputStream.getFormat();}public static FileItem createFileItem(String filePath, String fileName) {String fieldName = "file";FileItemFactory factory = new DiskFileItemFactory();FileItem item = factory.createItem(fieldName, "text/plain", true, fileName);File newfile = new File(filePath);int bytesRead = 0;byte[] buffer = new byte[8192];try (FileInputStream fis = new FileInputStream(newfile);OutputStream os = item.getOutputStream()) {while ((bytesRead = fis.read(buffer, 0, 8192)) != -1) {os.write(buffer, 0, bytesRead);}} catch (IOException e) {e.printStackTrace();}return item;}/*** 初始化客户端并返回key*/private static String initClientAndGetKey(AbstractUploadReqDTO dto, String uuid) {if (StringUtils.isAnyEmpty(dto.getFilename(), dto.getModel())) {throw new FileBizException("filename or model can not be empty");}BucketOwnerEnum bucketOwner = BucketOwnerEnum.valueOfType(dto.getBucketOwner());BucketTypeEnum bucketType = BucketTypeEnum.valueOfType(dto.getBucketType());if (BucketOwnerEnum.TENANT.equals(bucketOwner) && StringUtils.isBlank(dto.getTenantCode())) {throw new FileBizException("tenant code can not be empty");}initClient(bucketOwner, bucketType, dto.getTenantCode());return generateKey(commonClientBean.getBaseDir(),Objects.requireNonNull(bucketOwner), Objects.requireNonNull(bucketType),dto.getTenantCode(), dto.getModel(), dto.getPath(),uuid, dto.getFilename());}/*** 根据Key获取上传文件的Url*/@Overridepublic String getUrl(String key) {if (StringUtils.isNotEmpty(key)) {if (bucketType.equals(BucketTypeEnum.PUBLIC)) {//公有url(bindingDomain根据项目具体情况调整)if (StringUtils.isNotBlank(bindingDomain)) {return String.format("https://%s/%s", bindingDomain, key);}return String.format("https://%s/%s", bucketName + "." + endpoint, key);}//私有urlTemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.GET, expiration);request.setBucketName(bucketName);request.setObjectKey(key);TemporarySignatureResponse response = s3Client.createTemporarySignature(request);if (StringUtils.isNotBlank(bindingDomain)) {return response.getSignedUrl().replace(String.format("%s.%s", bucketName, endpoint), bindingDomain);}return response.getSignedUrl();}return null;}
}

8)封装公共请求参数-FileVoiceUploadReqDTO.java

@Data
@ApiModel("文件上传入参")
public class FileVoiceUploadReqDTO implements AbstractUploadReqDTO {@ApiModelProperty("文件")private MultipartFile file;@ApiModelProperty("字节数组")private byte[] bytes;@ApiModelProperty("租户code")private String tenantCode;@ApiModelProperty("文件名称")private String filename;@ApiModelProperty("文件权限范围; default:平台,tenant:租户; 若为tenant,tenantCode不能为空")private String bucketOwner = "tenant";@ApiModelProperty("桶类型; public:公有,private:私有")private String bucketType = "private";@ApiModelProperty("模块名称")@NotEmpty(message = "model cannot be empty")private String model;@ApiModelProperty("自定义路径")private String path;@ApiModelProperty("语音格式头:wav、mp3、pcm 默认:wav")private String audioFormat = "wav";@ApiModelProperty("采样率:16000、8000赫兹 默认:8000")private String sampleRate = "8000";@ApiModelProperty("语音合成特征字符串")private String property = "chinese_huaxiaomei_common";@ApiModelProperty("语速")private Integer speed = 0;@ApiModelProperty("音高")private Integer pitch = 0;@ApiModelProperty("音量")private Integer volume = 50;@ApiModelProperty("文本")private String text;}@ApiModel("上传入参父类")
public interface AbstractUploadReqDTO {String getTenantCode();String getFilename();String getBucketOwner();String getBucketType();String getModel();String getPath();}

9)业务类调用-ArticleManageController.java

这里注意异步容易丢失上下文,要在异步前将上下文获取RequestContextHolder.getRequestAttributes()

ArticleManageController.java

@Api(tags = {"文章管理"})
@Slf4j
@RestController
@RequestMapping("api/infoArticle")
public class ArticleManageController extends BaseController {
@ApiOperation(value = "新增保存", notes = "新增保存")@PostMapping("/save")public ResponseInteBean<Long> save(@RequestBody InfoArticleSaveOrUpdateReqVO articleSaveOrUpdateReqVO) {//......保存文章逻辑//接下来异步掉华为云语音,进行文章转语音播报//插入语音转换记录表,成功后再更改表中状态和urltry {//上传SISServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();infoCommandService.uploadFile(articleAddOrUpdateReqDTO,sra);} catch (Exception e) {log.error("save saveArticleVoice error:", e);}}}}return ResponseInteBean.ok(result.getData());}

ArticleManageService.java (这里实际调用了刚封装好的文件处理工具类FileUtils)

@Component
@Slf4j
public class ArticleManageServiceImpl implements ArticleManageService {@Async("threadPoolVoi")@Overridepublic void uploadFile(InfoArticleAddOrUpdateReqDTO infoArticleAddOrUpdateReqDTO, ServletRequestAttributes sra) {HttpServletRequest request = sra.getRequest();RequestContextHolder.setRequestAttributes(sra,true);//在异步方法调用之前手动传递请求上下文信息prepareUploadRequest(infoArticleAddOrUpdateReqDTO);}public void prepareUploadRequest(InfoArticleAddOrUpdateReqDTO infoArticleAddOrUpdateReqDTO) {log.info("异步处理语音播报 start");FileUploadResDTO fileUploadResDTO = new FileUploadResDTO();try {FileVoiceUploadReqDTO reqBody = new FileVoiceUploadReqDTO();//语音格式头:wav、mp3、pcm 默认:wavreqBody.setAudioFormat(audioFormat);//采样率,支持“8000”、“16000”,默认“8000”reqBody.setSampleRate(sampleRate);//语速取值范围:-500~500 默认值:0reqBody.setSpeed(speed);//音高 取值范围: -500~500 默认值:0reqBody.setPitch(pitch);//音量 取值范围:0~100 默认值:50reqBody.setVolume(volume);//语音合成特征字符串,组成形式为{language}_{speaker}_{domain},即“语种_人员标识_领域”reqBody.setProperty(property);reqBody.setPath("contentVoice");reqBody.setText(infoArticleAddOrUpdateReqDTO.getPureContent().replaceAll("[\\n\u00a0]+$", ""));reqBody.setTenantCode(infoArticleAddOrUpdateReqDTO.getTenantCode());reqBody.setBucketOwner(BucketOwnerEnum.DEFAULT.getType());reqBody.setBucketType(BucketTypeEnum.PUBLIC.getType());reqBody.setModel(ColumnConstants.CMS);reqBody.setFilename(infoArticleAddOrUpdateReqDTO.getArticleId() + ".wav");log.info("uploadFile to huawei sis start:{}", JSON.toJSONString(reqBody));fileUploadResDTO = FileUtils.convertToSpeechAndUploadObs(reqBody);log.info("uploadFile to huawei sis end:{}", fileUploadResDTO);} catch (Exception e) {log.error("upload text to speech  fail, articleId:{},text:{}", infoArticleAddOrUpdateReqDTO.getArticleId(), infoArticleAddOrUpdateReqDTO.getPureContent(), e);}//更新发布记录CmsContentVoiceRecordDO recordDO = new CmsContentVoiceRecordDO();if (null != fileUploadResDTO && StringUtils.isNotBlank(fileUploadResDTO.getUrl())) {recordDO.setVoiceStatus(ContentVoiceStatusEnum.STATUS_SUCCESS.getCode());} else {recordDO.setVoiceStatus(ContentVoiceStatusEnum.STATUS_FAILED.getCode());}recordDO.setArticleId(infoArticleAddOrUpdateReqDTO.getArticleId());recordDO.setContent(infoArticleAddOrUpdateReqDTO.getPureContent());recordDO.setFilePath(fileUploadResDTO.getUrl());recordDO.setFileName(fileUploadResDTO.getOriginalFilename());recordDO.setModifier(infoArticleAddOrUpdateReqDTO.getModifier());recordDO.setGmtModified(Calendar.getInstance().getTime());cmsContentVoiceRecordMapper.updateByArticleId(recordDO); log.info("异步开始处理语音播报 end");}

ThreadPoolVoice.java

@Configuration
public class ThreadPoolVoice {//定义线程池@Bean("threadPoolVoi") // bean的名称,线程池的bean的名字,不是创建线程的名字public Executor threadPoolVoi(){ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); /** 核心线程数(默认线程数) */executor.setMaxPoolSize(20);/** 最大线程数 */executor.setQueueCapacity(100);/** 缓冲队列大小 */executor.setKeepAliveSeconds(60);/** 允许线程空闲时间(单位:默认为秒) */executor.setWaitForTasksToCompleteOnShutdown(true);executor.setThreadNamePrefix("task-thread-voice-"); /** 线程池名前缀 */executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); //拒绝策略:缓存队列满了之后由调用线程处理,一般是主线程executor.initialize();//解决使用@Async注解,获取不到上下文信息的问题executor.setTaskDecorator(runnable -> {RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();return ()->{try {// 我们set 进去 ,其实是一个ThreadLocal维护的.RequestContextHolder.setRequestAttributes(requestAttributes);runnable.run();} finally {// 最后记得释放内存RequestContextHolder.resetRequestAttributes();}};});return executor;}
}

至此,文章转华为云语音播报的功能就实现了~

小结:整个过程需要注意的点:
1、异步请求上下文丢失问题(一些在异步线程里请求feign接口的也会产生丢失问题)
2、对可设置的参数的抽取和配置化,避免硬编码(比如nacos配置、yaml配置等)
3、使用@Async时建议自定义线程池。
@Async默认异步配置,指在@Async注解在使用时,不指定线程池的名称。使用的是SimpleAsyncTaskExecutor,该线程池默认执行任务都会创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。所以建议自定义线程池(比如上文中的“threadPoolVoi”)
4、语音转换注意发送内容时进行过滤校验,留下有实际语义的内容。(比如内容只有空格换行符等等发送给华为云,并不会转行成语音,会导致报错等)

相关文章:

华为云语音交互SIS的使用案例(文字转语音-详细教程)

文章目录 题记一 、语音交互服务&#xff08;Speech Interaction Service&#xff0c;简称SIS&#xff09;二、功能介绍1、实时语音识别2、一句话识别3、录音文件识别4、语音合成 三、约束与限制四、使用1、API2、SDK 五、项目集成1、引入pom依赖2、初始化 Client1&#xff09;…...

设计一个监控摄像头物联网IOT(webRTC、音视频、文件存储)

前言&#xff1a; 设计一个完整的 监控摄像头物联网 IoT 平台 涉及 视频直播和点播、WebRTC 和 文件存储模块&#xff0c;可以分为以下几个主要部分&#xff1a;摄像头设备、服务端处理、Web 前端、视频流存储和回放。以下是结合这些技术的一个具体完整流程设计&#xff0c;涵盖…...

学习笔记(prism--视频【WPF-prism核心教程】)--待更新

《一》框架介绍 prism是一个用于WPF…和winUI中构建的松散耦合&#xff0c;可维护和可测试的应用程序框架。帮助WPF开发人员以简化编写&#xff0c;维护和扩展来设计应用程序。 优点&#xff1a;遵循特定的约定&#xff0c;可自动将view/ViewModel建立DataContext的关系&#…...

Kafka无锁设计

前言 在分布式消息队列系统中,Kafka 的无锁设计是其高吞吐量和高并发的核心优势之一。通过避免锁的竞争,Kafka 能够在高并发和大规模的生产环境中保持高效的性能。为了更好地理解 Kafka 的无锁设计,我们首先对比传统的队列模型,然后探讨 Kafka 如何通过无锁机制优化生产者…...

【GO基础学习】gin框架路由详解

文章目录 gin框架路由详解&#xff08;1&#xff09;go mod tidy&#xff08;2&#xff09;r : gin.Default()&#xff08;3&#xff09;r.GET()路由注册 &#xff08;4&#xff09;r.Run()路由匹配 总结 gin框架路由详解 先创建一个项目&#xff0c;编写一个简单的demo&#…...

GPIO+TIM(无PWM)实现呼吸灯功能

程序特点&#xff1a; 1、模块化&#xff0c;可快速移植&#xff0c;5分钟便可完成移植。 2、通过GPIO普通定时器&#xff0c;实现呼吸灯功能。 3、PWM周期为5ms&#xff0c;占空比调节时间为20ms&#xff0c;占空比为100等份&#xff0c;即呼吸灯从暗到亮需要20ms*1002s。 …...

贪心算法.

贪心算法是指只从当前角度出发,做出当前情景下最好的选择,在某种意义上来说是局部最优解,并不从全局的角度做决策.如果贪心策略选择不恰当,可能无法得到全局最优解. 贪心算法的基本流程如下: 1.分析问题,确定优化目标,对变量进行初始化 2.制定贪心策略:在制定贪心策略时需要…...

Linux系统和makefile详解

### Linux系统详解 Linux是一个开源且功能强大的操作系统内核&#xff0c;自1991年由林纳斯托瓦兹首次发布以来&#xff0c;它已经成为全球最流行的操作系统之一。Linux的核心特性包括开源、多用户多任务、高稳定性与安全性&#xff0c;以及良好的跨平台能力。 1. **开源**&a…...

GitLab 将停止为中国区用户提供服务,60天迁移期如何应对? | LeetTalk Daily

“LeetTalk Daily”&#xff0c;每日科技前沿&#xff0c;由LeetTools AI精心筛选&#xff0c;为您带来最新鲜、最具洞察力的科技新闻。 GitLab作为一个广受欢迎的开源代码托管平台&#xff0c;近期宣布将停止服务中国大陆、澳门和香港地区的用户提供服务。根据官方通知&#x…...

【杂谈】-AI搜索引擎如何改变传统SEO及其在内容营销中的作用

AI搜索引擎如何改变传统SEO及其在内容营销中的作用 文章目录 AI搜索引擎如何改变传统SEO及其在内容营销中的作用1、什么是AI搜索引擎2、AI搜索引擎对SEO策略的影响3、AI搜索引擎在内容营销转型中的作用4、AI搜索引擎在营销领域的挑战、道德问题和未来5、总结 在当今的数字营销世…...

PTA数据结构编程题7-1最大子列和问题

我参考的B站up的思路 题目 题目链接 给定K个整数组成的序列{ N 1 ​ , N 2 ​ , …, N K ​ }&#xff0c;“连续子列”被定义为{ N i ​ , N i1 ​ , …, N j ​ }&#xff0c;其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 1…...

深入浅出:AWT的基本组件及其应用

目录 前言 1. AWT简介 2. AWT基本组件 2.1 Button&#xff1a;按钮 2.2 Label&#xff1a;标签 ​编辑 2.3 TextField&#xff1a;文本框 2.4 Checkbox&#xff1a;复选框 2.5 Choice&#xff1a;下拉菜单 2.6 List&#xff1a;列表 综合案例 注意 3. AWT事件处理 …...

MySQL45讲 第三十六讲 为什么临时表可以重名?——阅读总结

文章目录 MySQL45讲 第三十六讲 为什么临时表可以重名&#xff1f;——阅读总结一、引言二、临时表与内存表的区别&#xff08;一&#xff09;内存表&#xff08;二&#xff09;临时表 三、临时表的特性&#xff08;一&#xff09;可见性与生命周期&#xff08;二&#xff09;与…...

WebRTC服务质量(11)- Pacer机制(03) IntervalBudget

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…...

.NET常用的ORM框架及性能优劣分析总结

市面上有很多流行的 ORM&#xff08;对象关系映射&#xff09;框架可以用于 .NET 开发。本文主要针对以下几种常见的 ORM 框架&#xff0c;对其优劣进行分析及总结&#xff0c;希望能够帮助大家进行ORM框架的使用有所帮助。 1. Entity Framework (EF) 特点 • 官方支持&…...

Ubuntu网络配置(桥接模式, nat模式, host主机模式)

windows上安装了vmware虚拟机&#xff0c; vmware虚拟机上运行着ubuntu系统。windows与虚拟机可以通过三种方式进行通信。分别是桥接模式&#xff1b;nat模式&#xff1b;host模式 一、桥接模式 所谓桥接模式&#xff0c;也就是虚拟机与宿主机处于同一个网段&#xff0c; 宿主机…...

光通信复习

第一章 1.5 光纤通信系统的基本组成是怎么样的&#xff1f;试画出简图予以说明 光纤&#xff1a;主要负责光信号的传输光发送器&#xff1a;将用户端的电信号转化为光信号&#xff0c;入射到光纤内部光中继器&#xff1a;将光纤中发生衰减和畸变的光信号变成没有衰减和畸变的原…...

数字化转型中的投资决策:IT平台投资与业务应用投资的思考

在数字化转型的大潮中&#xff0c;企业常常面临一个核心问题&#xff1a;如何在繁杂的投资决策中精准地分配资源&#xff0c;特别是在IT平台投资和业务应用投资之间&#xff0c;如何合理划分责任与投入&#xff1f;在一些大型企业中&#xff0c;尤其是华为&#xff0c;针对不同…...

Linux快速入门-Linux的常用命令

Linux的常用命令 1. Linux的终端与工作区1.1 终端概述1.2 切换终端 2. Shell语言解释器2.1 Shell概述 3. 用户登录与身份切换3.1 su 命令3.2 sudo 命令 4. 文件、目录操作命令4.1 pwd 命令4.2 cd 命令4.3 ls 命令4.3.1 ls 指令叠加使用 4.4 mkdir 命令4.5 rmdir 命令4.6 cp 命令…...

【ORB-SLAM3:相机针孔模型和相机K8模型】

在ORB-SLAM3中&#xff0c;相机的建模是 SLAM 系统的核心之一&#xff0c;因为它直接影响到如何处理和利用图像数据进行定位和地图构建。ORB-SLAM3 支持不同的相机模型&#xff0c;其中包括针孔模型和鱼眼模型&#xff08;K8 模型&#xff09;。下面分别介绍这两种模型。 相机…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径

目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/

使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题&#xff1a;docker pull 失败 网络不同&#xff0c;需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

聊一聊接口测试的意义有哪些?

目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开&#xff0c;首…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

vulnyx Blogger writeup

信息收集 arp-scan nmap 获取userFlag 上web看看 一个默认的页面&#xff0c;gobuster扫一下目录 可以看到扫出的目录中得到了一个有价值的目录/wordpress&#xff0c;说明目标所使用的cms是wordpress&#xff0c;访问http://192.168.43.213/wordpress/然后查看源码能看到 这…...

华为OD机考-机房布局

import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...