电商网站如何备案,大气自适应网站源码,东莞做网站最好的是哪家,wordpress批量审核文章目录 题记一 、语音交互服务#xff08;Speech Interaction Service#xff0c;简称SIS#xff09;二、功能介绍1、实时语音识别2、一句话识别3、录音文件识别4、语音合成 三、约束与限制四、使用1、API2、SDK 五、项目集成1、引入pom依赖2、初始化 Client1#xff09;… 文章目录 题记一 、语音交互服务Speech Interaction Service简称SIS二、功能介绍1、实时语音识别2、一句话识别3、录音文件识别4、语音合成 三、约束与限制四、使用1、API2、SDK 五、项目集成1、引入pom依赖2、初始化 Client1准备参数2nacos配置3配置类-CommonClientsProperties.java4初始化客户端配置-CommonClientsCache.java5抽取公共文件客户端封装对象- CommonClientBean.java6华为云语音生成客户端封装-HuaweiClientBean.java7工具类-FileUtils.java8封装公共请求参数-FileVoiceUploadReqDTO.java9业务类调用-ArticleManageController.java 题记
本文将根据一种具体业务场景语音播报将一篇ai撰写的文章异步转换成语音文件进行播报为案例演示华为云语音交互SIS的集成使用。
一 、语音交互服务Speech Interaction Service简称SIS
语音交互服务Speech Interaction Service简称SIS是一种人机交互方式用户通过实时访问和调用APIApplication Programming Interface应用程序编程接口将语音识别成文字或者将文本转换成逼真的语音等。 常用的应用场景参看官网应用场景 二、功能介绍 Tip:根据你的需求场景是否实时、大小、时长、是语音转文字还是文字转语音等等评估应该使用下边哪种功能。 1、实时语音识别
实时语音识别服务用户通过实时访问和调用API获取实时语音识别结果支持的语言包含中文普通话、方言和英语方言当前支持四川话、粤语和上海话。 文本时间戳 为音频转换结果生成特定的时间戳从而通过搜索文本即可快速找到对应的原始音频。 智能断句 通过提取上下文相关语义特征并结合语音特征智能划分断句及添加标点符号提升输出文本的可阅读性。 中英文混合识别 支持在中文句子识别中夹带英文字母、数字等从而实现中、英文以及数字的混合识别。 即时输出识别结果 连续识别语音流内容即时输出结果并可根据上下文语言模型自动校正。 自动静音检测 对输入语音流进行静音检测识别效率和准确率更高。
2、一句话识别
可以实现1分钟以内音频到文字的转换。对于用户上传的二进制音频格式数据系统经过处理生成语音对应的文字支持的语言包含中文普通话、方言以及英语。方言当前支持四川话、粤语和上海话。
3、录音文件识别
对于录制的长语音进行识别转写成文字提供不同领域模型具备良好的可扩展性支持热词定制。
4、语音合成
文本转成语音语音合成支持多种音色可调节语调语速音量。 这里我将使用【4、语音合成】功能实现开篇提到的文章转语音播报的目的。 三、约束与限制
明确了要使用的功能接下来看有哪些约束限制是否与需求契合。使用【语音合成】功能的注意点
支持“华北-北京四”、“华东-上海一”区域。支持中文、英文、中英文文本不长于500个字符。支持合成采样率8kHz、16kHz。 Tip:由上可知如果文本大于500字符就需要切割再合并问题。 以上了解了需求场景能不能使用接下来就看怎么用啦~
四、使用
主要有两种接入方式API或SDK。
1、API
SIS服务提供了两种接口包含RESTRepresentational State TransferAPI支持您通过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依赖 dependencygroupIdcom.huaweicloud.sdk/groupIdartifactIdhuaweicloud-sdk-sis/artifactIdversion3.1.116/versionexclusionsexclusiongroupIdcom.fasterxml.jackson.dataformat/groupIdartifactIdjackson-dataformat-xml/artifactId/exclusion/exclusions/dependency2、初始化 Client 注意官方文档上显示的客户端client可能是未更新的或者和你本地引入的依赖里的客户端不匹配根据实际情况使用你依赖里的客户端去处理就好以及封装的请求对象。 【我这里依赖里的客户端是SisClient请求类RunTtsRequest】 1准备参数
首先需要一些认证信息、配置信息可参考官网获取方式 请求参数 目前SDK仅支持AK/SK认证方式。 2nacos配置
我们将上边的信息以及可以调整的参数统一提取出来配置化避免硬编码这里我统一放到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: 443配置类-CommonClientsProperties.java
CommonClientsProperties.java
ConfigurationProperties(prefix common)
public class CommonClientsProperties {private ListProperties clients new ArrayList();public ListProperties getClients() {return clients;}public void setClients(ListProperties 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 MapString, CommonClientBean cache new HashMap();PostConstructpublic void init() {ListCommonClientsProperties.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 ListString VOICE_FORMATS Arrays.asList(wav, mp3, pcm);/*** 采样率支持“8000”、“16000”*/public static final ListString 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(Listbyte[] 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 ResponseInteBeanLong 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、语音转换注意发送内容时进行过滤校验留下有实际语义的内容。比如内容只有空格换行符等等发送给华为云并不会转行成语音会导致报错等