华为云语音交互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字符就需要切割再合并问题。
以上了解了需求场景能不能使用,接下来就看怎么用啦~
四、使用
主要有两种接入方式:API
或SDK
。
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的使用案例(文字转语音-详细教程)
文章目录 题记一 、语音交互服务(Speech Interaction Service,简称SIS)二、功能介绍1、实时语音识别2、一句话识别3、录音文件识别4、语音合成 三、约束与限制四、使用1、API2、SDK 五、项目集成1、引入pom依赖2、初始化 Client1)…...
【Rust自学】6.3. 控制流运算符-match
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 6.3.1. 什么是match match允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码。模式可以是字面值、变量名、通配符等…...
AIA - IMSIC之二(附IMSIC处理流程图)
本文属于《 RISC-V指令集基础系列教程》之一,欢迎查看其它文章。 1 通过IMSIC接收外部中断的CSR 软件通过《AIA - 新增的CSR》描述的CSR来访问IMSIC。 machine level 的 CSR 与 IMSIC 的 machine level interrupt file 可相互互动;而 supervisor level 的 CSR…...
Excel中一次查询返回多列
使用Excel或wps的时候,有时候需要一次查询返回多列内容,这种情况可以选择多次vlookup或者多次xlookup,但是这种做法费时费力不说,效率还有些低下,特别是要查询的列数过多时。我放了3种查询方法,效果图&…...
SQLAlchemy示例(连接数据库插入表数据)
背景需求 连接数据库,插入表中一些数据。 其用户是新建用户,所以只能插入,不能更新。 再次输入数据则使用更新数据语法,这个没调试。 #! /usr/bin/env python # -*- coding: utf-8 -*-from sqlalchemy import create_engine, …...
AG32 MCU 的电机控制方案
原创 AG32 AG32MCU cpld 2024年12月24日 17:23 浙江 AG32 MCU 的电机控制方案 在工业自动化、智能家居、新能源设备等众多领域,电机控制的精准性、稳定性和高效性至关重要。 AG32 MCU 凭借其高性能处理器、丰富的外设资源以及独特的 2K CPLD 资源,在电机…...
Linux:进程概念
1.冯诺依曼体系结构 结论: --- CPU不和外设直接打交道,和内存直接打交道。 --- 所有的外设,有数据需要收入,只能载入到内存中;内存写出,也一定是写道外设中。 --- 为什么程序要运行必须加载到内存…...
使用 Webpack 优雅的构建微前端应用❕
Module Federation 通常译作“模块联邦”,是 Webpack 5 新引入的一种远程模块动态加载、运行技术。MF 允许我们将原本单个巨大应用按我们理想的方式拆分成多个体积更小、职责更内聚的小应用形式,理想情况下各个应用能够实现独立部署、独立开发(不同应用甚…...
【Leetcode 热题 100】208. 实现 Trie (前缀树)
问题背景 T r i e Trie Trie 或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。 请你实现 Trie 类: Trie() 初始化前缀树对象。void insert(String word…...
从0开始在linux服务器上部署SpringBoot和Vue
目录 一、申请服务器的IP (1)阿里云申请IP (2)设置服务器的密码 (3)远程终端——MobaXterm 二、Docker (1)安装Docker (2)镜像加速 (3&…...
41 stack类与queue类
目录 一、简介 (一)stack类 (二)queue类 二、使用与模拟实现 (一)stack类 1、使用 2、OJ题 (1)最小栈 (2)栈的弹出压入序列 (3…...
代码随想录-笔记-其八
让我们开始:动态规划! 70. 爬楼梯 - 力扣(LeetCode) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? class Solution { public:int climbStairs(i…...
信号仿真高级工程师面试题
信号仿真高级工程师面试题可能涵盖多个方面,旨在全面评估应聘者的专业知识、技能水平、实践经验和问题解决能力。以下是一些可能的面试题及其简要解析: 一、专业知识与技能 描述你对信号仿真的理解 考察点:对信号仿真基本概念、原理及应用的掌握程度。参考答案:信号仿真是…...
FLTK - build fltk-1.1.10 on vs2019
文章目录 FLTK - build fltk-1.1.10 on vs2019概述笔记buildtest测试程序运行 END FLTK - build fltk-1.1.10 on vs2019 概述 看书上用到了fltk-1.1.10, 用vs2019试试能否正常编译使用? 笔记 build 从官网下载fltk-1.1.10-source.tar.bz2 用7zip解开 fltk-1.1.10-source.…...
FPGA远程升级 -- FLASH控制
简介 前文讲到如何实现XILINX芯片程序跳转,但升级程序是事先通过VIVADO工具将两个程序合成一个BIN文件实现升级的,并不能在线更新升级。要实现远程升级的能力需要对FPGA的FLASH进行在线写入升级程序。 FLASH介绍 本次设计FLASH选用的是S25FL128芯片&…...
蓝牙BLE开发——解决iOS设备获取MAC方式
解决iOS设备获取MAC方式 uniapp 解决 iOS 获取 MAC地址,在Android、iOS不同端中互通,根据MAC 地址处理相关的业务场景; 文章目录 解决iOS设备获取MAC方式监听寻找到新设备的事件BLE工具效果图APP监听设备返回数据解决方式ArrayBuffer转16进制…...
【总结(三)】单片机重点知识总结记录(串口重定向+按键消抖+延时)
一.串口重定向 串口重定向代码如下 注意: 要添加头文件include "stdio.h"要勾选微库,即Use MicroLIB /**********重定向************/ //串口1 int fputc(int ch, FILE *f) {HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff)…...
攻防世界 unserialize3
开启场景 题目为unserialize3,这个单词在php中代表反序列化,代码 __wakeup 也是php反序列化中常见的魔术方法,所以这个题基本就是和反序列化有关的题目。根据代码提示,编写一个Exploit运行,将对象xctf的信息序列化 得到…...
ISDP010_基于DDD架构实现收银用例主成功场景
信息系统开发实践 | 系列文章传送门 ISDP001_课程概述 ISDP002_Maven上_创建Maven项目 ISDP003_Maven下_Maven项目依赖配置 ISDP004_创建SpringBoot3项目 ISDP005_Spring组件与自动装配 ISDP006_逻辑架构设计 ISDP007_Springboot日志配置与单元测试 ISDP008_SpringB…...
如何注册华为云国际版账户:详细步骤指南
华为云作为全球知名的云计算服务提供商,提供了丰富的云服务和解决方案。无论是企业还是个人开发者,注册华为云国际版账户都是开启云计算之旅的第一步。我们九河云通过本文将为您详细介绍华为云国际版的注册流程。 第一步:访问华为云国际版官网…...
存储过程实现多个分类不同计算规则得到对应的分类、月份和款号
该存储过程 PRO_MON_MDCODE 实现多个分类不同计算规则得到对应的分类、月份和款号,其中线下分类的款最早出现时间会在20230101,最晚是当前月份后12月,电商的款取商品维表的23,24,25年商品年份的A款,其他业务分类逻辑(A-线上,B电商公司,C品牌公司)的款最早出现时间会在2…...
【LeetCode】906、超级回文数
【LeetCode】906、超级回文数 文章目录 一、通过数据量猜解法 枚举 数学 回文1.1 通过数据量猜解法 枚举 数学 回文1.2 多语言解法 二、打表法 一、通过数据量猜解法 枚举 数学 回文 1.1 通过数据量猜解法 枚举 数学 回文 减小数据规模: 先构成回文, 再平方, 再判断是否是范围…...
使 el-input 内部的内容紧贴左边
<el-inputv-model"form.invitor"placeholder"PC端的自动取当前账号的手机号"readonlyclass"no-border-input" />::v-deep(.no-border-input .el-input__inner) { border: none; box-shadow: none; padding-left: 0; /* 确保内容紧贴左边 *…...
【ES6复习笔记】模板字符串(3)
介绍 模板字符串是 ES6 引入的一种新的字符串声明方式,它使用反引号()来定义字符串,而不是单引号()或双引号(")。模板字符串可以包含变量、表达式和换行符,这使得它…...
Linux学习
Linux Linux目录结构 Linux只有一个顶级目录,称之为:根目录 /在Linux系统中表示 出现在开头的/表示:根目录 出现在后面的/表示:层次关系 Linux命令基础 什么是命令、命令行 命令:即Linux操作指令,是系…...
【PostgreSQL使用】最新功能逻辑复制槽的failover,大数据下高可用再添利器
逻辑复制的failover 专栏内容: postgresql入门到进阶手写数据库toadb并发编程 个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. ✅ ὒ…...
MongoDB 创建用户、User、Role 相关 操作
创建用户 # db.createUser() Creates a new user.详细 查看 db.createUser() - MongoDB Manual v8.0 设置用户 Role(创建用户时也可以设置) # db.grantRolesToUser() Grants a role and its privileges to a user. 详细 查看 db.grantRolesToUser(…...
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
一、概述 记录时间 [2024-12-26] 本文讲述如何在 Vue3 项目中使用计时器组件。具体包括开发环境的配置,ViteVue 项目的创建,Element Plus 插件的使用,以及计时器组件的创建和使用。 想要直接实现计时器组件,查看文章的第四部分。…...
Redis单线程快的原因
基于内存操作:Redis将数据存储在内存中,使得数据的读写速度极快,这是其性能优势的主要原因。单线程避免上下文切换:在多线程环境下,CPU核数有限,线程上下文切换会带来性能损耗。Redis采用单线程,…...
基于SpringBoot的“在线BLOG网”的设计与实现(源码+数据库+文档+PPT)
基于SpringBoot的“在线BLOG网”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 在线BLOG网结构功能图 管理员登录功能界面 用户信息…...
重温设计模式--6、享元模式
文章目录 享元模式(Flyweight Pattern)概述享元模式的结构C 代码示例1应用场景C示例代码2 享元模式(Flyweight Pattern)概述 定义: 运用共享技术有效地支持大量细粒度的对象。 享元模式是一种结构型设计模式࿰…...
springboot-starter版本升级es版本问题
一、背景说明 版本漏洞处理,springboot版本升级,es版本暂不升级,但是pom引用中es版本一直为7.17.15高版本,不想显示声明版本,定位具体问题,最后还是重新定义了版本进行处理。 二、异常情况 这里看4.4.18是…...
嵌入式科普(25)Home Assistant米家集成意味着IOT的核心是智能设备
目录 一、概述 二、一张图说尽HA 三、HA的相关资料 四、米家集成划重点 五、总结 一、概述 小米Home Assistant 米家集成开源一周star近15k,迭代4个版本,12个贡献者 本文科普一下Home Assistant(简称HA)、米家集成ÿ…...
前端Python应用指南(四)Django实战:创建一个简单的博客系统
《写给前端的python应用指南》系列: (一)快速构建 Web 服务器 - Flask vs Node.js 对比(二)深入Flask:理解Flask的应用结构与模块化设计(三)Django vs Flask:哪种框架适…...
webrtc获取IceCandidate流程
在WebRTC(Web Real-Time Communication)中,ICECandidate是一个关键概念,它用于描述在建立点对点(P2P)连接时可以考虑的潜在通信端点。以下是关于WebRTC中ICECandidate的详细解释: 一、ICECandidate的定义 ICECandidate对象通常包含以下关键属性: foundation:用于唯一…...
快速入门Spring AI Alibaba
文章目录 前言一、环境准备二、获取API Key三、代码示例pomapplication.ymlcontroller 前言 Spring Cloud Alibaba AI 以 Spring AI 为基础,并在此基础上,基于 Spring AI 0.8.1 版本 API 完成同义系列大模型的接入实现阿里云同义系列大模型全面适配。 在…...
【mysql】MVCC及实现原理
【mysql】MVCC及实现原理 【一】介绍【1】什么是MVCC【2】什么是当前读和快照读【3】当前读,快照读和MVCC的关系【4】MVCC 能解决什么问题,好处(1)数据库并发场景有三种,分别为:(2)M…...
C++ —— 模板类与函数
C —— 模板类与函数 模板类可以用于函数的参数和返回值,有三种形式: 普通函数,参数和返回值是模板类的实例化版本。函数模板,参数和返回值是某种的模板类。函数模板,参数和返回值是任意类型(支持普通类和…...
【笔记】下载mysql5.7
MySQL5.7安装超详细步骤(保姆级教程)_mysql5.7下载安装-CSDN博客 下载与安装 初次登录以及改密码 MySQL 5.7 安装教程(全步骤图解教程)_mysql5.7的安装教程-CSDN博客...
让 AMD GPU 在大语言模型推理中崭露头角:机遇与挑战
在当今科技飞速发展的时代,大语言模型(LLM)的兴起彻底改变了人工智能领域的格局。从智能客服到文本生成,从知识问答到代码编写辅助,大语言模型的应用无处不在,深刻影响着我们的生活和工作。然而,…...
Linux运维常见命令
vi/vim快捷键使用 1)拷贝当前行 yy ,拷贝当前行向下的5行 5yy,并粘贴(输入p)。 2)删除当前行 dd ,删除当前行向下的5行5dd 3)在文件中查找某个单词 [命令行下 /关键字,回车查找 ,输入n就是查找下一个 ] 4)设置文件的行号&…...
前端真实面试题自用
一、写在前面 笔者,经过计算机学硕考研的失败后,想谋求一份前端工作实在是太难了。一方面,确实曾经学习过的东西很久没有拾起,另一方面,对于前端面经还是记忆不深刻,特地写此贴记录笔者在真实前端面试中遇…...
人工智能与云计算的结合:如何释放数据的无限潜力?
引言:数据时代的契机 在当今数字化社会,数据已成为推动经济与技术发展的核心资源,被誉为“21世纪的石油”。从个人消费行为到企业运营决策,再到城市管理与国家治理,每个环节都在生成和积累海量数据。然而,数…...
初始 ShellJS:一个 Node.js 命令行工具集合
一. 前言 Node.js 丰富的生态能赋予我们更强的能力,对于前端工程师来说,使用 Node.js 来编写复杂的 npm script 具有明显的 2 个优势:首先,编写简单的工具脚本对前端工程师来说额外的学习成本很低甚至可以忽略不计,其…...
benchmarksql5.0 测试工具如何在达梦上能够跑起来?
(1)添加驱动程序文件: lib文件夹下创建一个dm文件夹,并在dm文件夹下放置达梦数据库的驱动程序:DmJdbcDriver8.jar (2)引用驱动程序文件所在目录: 在文件benchmarksql-5.0/run/funcs.sh中的function setCP()函数中; 也就是在文件…...
amazon广告授权
amazon授权资料地址 Amazon Advertising Advanced Tools Center postman地址:Amazon Advertising Advanced Tools Center...
设计模式与游戏完美开发(2)
更多内容可以浏览本人博客:https://azureblog.cn/ 😊 该文章主体内容来自《设计模式与游戏完美开发》—蔡升达 第二篇 基础系统 第四章 游戏主要类——外观模式(Facade) 一、游戏子功能的整合 一个游戏程序常常由内部数个不同的…...
WebRTC服务质量(10)- Pacer机制(02) RoundRobinPacketQueue
WebRTC服务质量(01)- Qos概述 WebRTC服务质量(02)- RTP协议 WebRTC服务质量(03)- RTCP协议 WebRTC服务质量(04)- 重传机制(01) RTX NACK概述 WebRTC服务质量(…...
VSCode调试
目录 C/C远程本地调试插件配置参考 C/C远程本地调试 测试源码:https://github.com/jrhee17/ssl-study 插件 Remote - SSH C/C 配置 .vscode/launch.json {"version": "0.2.0","configurations": [{"name": "afte…...
【ES6复习笔记】解构赋值(2)
介绍 解构赋值是一种非常方便的语法,可以让我们更简洁地从数组和对象中提取值,并且可以应用于很多实际开发场景中。 1. 数组的解构赋值 数组的解构赋值是按照一定模式从数组中提取值,然后对变量进行赋值。下面是一个例子: con…...