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

news2024/12/24 11:31:05

文章目录

  • 题记
  • 一 、语音交互服务(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.com
      accessKeyId: YL6BxxxxxxxxxxxxxxxKL
      accessKeySecret: w0pTVxxxxxxxxxxxxxx1hXnH
      projectId: 0744xxxxxxxxxxxxxd9a
      region: cn-north-4
  default:
    #默认的私有桶url有效时间,单位:秒。
    expiration: 3600
    #租户备用桶设置(只支持读取)
    buckets:
        #租户code
      - tenantCode: test
        #{bucketOwner}_{bucketType},根据bucketOwner和bucketType映射到上面配置的桶
        spareBucket: test_public


#华为云语音合成音色设置    
sis-client:
   #语音格式头:wav、mp3、pcm 默认:wav
   audioFormat: wav
   #采样率:16000、8000赫兹 默认:8000
   sampleRate: 8000
   #语音合成特征字符串
   property: chinese_huaxiaodong_common
   #语速
   speed: 0
   #音高
   pitch: 43
   #音量默认50
   volume: 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;
    }

    @Data
    public 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 {

    @Resource
    CommonClientsProperties commonClientsProperties;
    private final Map<String, CommonClientBean> cache = new HashMap<>();

    @PostConstruct
    public 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 默认:wav
        configBody.setAudioFormat(paramConfig.getAudioFormat());
        //采样率,支持“8000”、“16000”,默认“8000”
        configBody.setSampleRate(paramConfig.getSampleRate());
        //语速取值范围:-500~500 默认值:0
        configBody.setSpeed(paramConfig.getSpeed());
        //音高 取值范围: -500~500 默认值:0
        configBody.setPitch(paramConfig.getPitch());
        //音量 取值范围:0~100 默认值:50
        configBody.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
     */
    @Override
    public 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");
        }
    }
   /**
     * 上传输入流
     */
    @Override
    public 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.上传语音文件到obs
            FileItem 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
     */
    @Override
    public 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);
            }
            //私有url
            TemporarySignatureRequest 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) {
 			 //......保存文章逻辑
 			 //接下来异步掉华为云语音,进行文章转语音播报
            //插入语音转换记录表,成功后再更改表中状态和url
                        try {
                          
                            //上传SIS
                            ServletRequestAttributes 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")
    @Override
    public 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 默认:wav
            reqBody.setAudioFormat(audioFormat);
            //采样率,支持“8000”、“16000”,默认“8000”
            reqBody.setSampleRate(sampleRate);
            //语速取值范围:-500~500 默认值:0
            reqBody.setSpeed(speed);
            //音高 取值范围: -500~500 默认值:0
            reqBody.setPitch(pitch);
            //音量 取值范围:0~100 默认值:50
            reqBody.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、语音转换注意发送内容时进行过滤校验,留下有实际语义的内容。(比如内容只有空格换行符等等发送给华为云,并不会转行成语音,会导致报错等)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2264695.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GitCode 光引计划投稿|智能制造一体化低代码平台 Skyeye云

随着智能制造行业的快速发展&#xff0c;企业对全面、高效的管理解决方案的需求日益迫切。然而&#xff0c;传统的开发模式往往依赖于特定的硬件平台&#xff0c;且开发过程繁琐、成本高。为了打破这一瓶颈&#xff0c;Skyeye云应运而生&#xff0c;它采用先进的低代码开发模式…

网络刷卡器的功能和使用场景

网络刷卡器是一种连接互联网的设备&#xff0c;能够通过网络将读取到的各种卡片信息传输至服务器进行处理。这类刷卡器通常支持多种类型的卡片&#xff0c;如银行卡、身份证、会员卡、公交卡等&#xff0c;并运用现代信息技术确保数据的安全性和高效性&#xff0c;功能十分强大…

从零开始C++游戏开发之第七篇:游戏状态机与回合管理

在游戏开发的道路上&#xff0c;状态管理是一个无法绕开的重要课题。尤其是在棋牌类游戏中&#xff0c;游戏的进行需要有条不紊地按照回合推进&#xff0c;同时管理多个游戏状态&#xff0c;如“等待玩家加入”、“游戏进行中”、“结算阶段”等。如何优雅且高效地实现这些逻辑…

有没有检测吸烟的软件 ai视频检测分析厂区抽烟报警#Python

在现代厂区管理中&#xff0c;安全与规范是重中之重&#xff0c;而吸烟行为的管控则是其中关键一环。传统的禁烟管理方式往往依赖人工巡逻&#xff0c;效率低且存在监管死角&#xff0c;难以满足当下复杂多变的厂区环境需求。此时&#xff0c;AI视频检测技术应运而生&#xff0…

CentOS7网络配置,解决不能联网、ping不通外网、主机的问题

1. 重置 关闭Centos系统 编辑->虚拟网络编辑器 还原默认设置 2. 记录基本信息 查看网关地址,并记录在小本本上 查看网段,记录下 3. 修改网卡配置 启动Centos系统 非root用户,切换root su root查看Mac地址 ifconfig 或 ip addr记录下来 修改配置文件 vim /et…

32岁前端干了8年,是继续做前端开发,还是转其它工作

前端发展有瓶颈&#xff0c;变来变去都是那一套&#xff0c;只是换了框架换了环境。换了框架后又得去学习&#xff0c;虽然很快上手&#xff0c;但是那些刚毕业的也很快上手了&#xff0c;入门门槛越来越低&#xff0c;想转行或继续卷&#xff0c;该如何破圈 这是一位网友的自述…

麒麟操作系统服务架构保姆级教程(三)ssh远程连接

如果你想拥有你从未拥有过的东西&#xff0c;那么你必须去做你从未做过的事情 作为一名成熟运维架构师&#xff0c;我们需要管理的服务器会达到几十台&#xff0c;上百台&#xff0c;上千台&#xff0c;甚至是上万台服务器&#xff0c;而且咱们的服务器还不一定都在一个机房&am…

Hmsc包开展群落数据联合物种分布模型分析通用流程(Pipelines)

HMSC&#xff08;Hierarchical Species Distribution Models&#xff09;是一种用于预测物种分布的统计模型。它在群落生态学中的应用广泛&#xff0c;可以帮助科学家研究物种在不同环境条件下的分布规律&#xff0c;以及预测物种在未来环境变化下的潜在分布范围。 举例来说&a…

十二月第22讲:巧用mask属性创建一个纯CSS图标库

&#xff08;Scalable Vector Graphics&#xff0c;可缩放矢量图形&#xff09;是一种基于 XML 的图像格式&#xff0c;用于定义二维图形。与传统的位图图像&#xff08;如 PNG 和 JPG&#xff09;不同&#xff0c;SVG 图像是矢量图形&#xff0c;可以在任何尺寸下保持清晰度&a…

单片机:实现驱动超声波(附带源码)

单片机实现驱动超声波模块 超声波模块&#xff08;如HC-SR04&#xff09;广泛应用于距离测量、避障系统、自动驾驶等嵌入式项目中。它能够通过发射超声波信号并接收反射波来计算物体的距离。本文将介绍如何使用单片机&#xff08;如51系列单片机&#xff09;驱动超声波模块&am…

封装(2)

大家好&#xff0c;今天我们来介绍一下包的概念&#xff0c;知道包的作用可以更好的面对今后的开发&#xff0c;那么我们就来看看包是什么东西吧。 6.3封装扩展之包 6.3.1包的概念 在面向对象体系中,提出了一个软件包的概念,即:为了更好的管理类,把多个类收集在一起成为一组…

重温设计模式--命令模式

文章目录 命令模式的详细介绍C 代码示例C代码示例2 命令模式的详细介绍 定义与概念 命令模式属于行为型设计模式&#xff0c;它旨在将一个请求封装成一个对象&#xff0c;从而让你可以用不同的请求对客户端进行参数化&#xff0c;将请求的发送者和接收者解耦&#xff0c;并且能…

基于STM32U575RIT6的智能除湿器

项目说明 除湿器原理 知识点 GPIO、定时器、中断、ADC、LCD屏幕、SHT20、SPI、IIC、UART 功能概述 模块功能LCD屏幕显示温湿度&#xff0c;风机开关情况&#xff0c;制冷 开关情况&#xff0c;加热片开关情况&#xff0c;温 湿度上下阈值&#xff0c;设备ID&#xff0c;电…

【电商搜索】CRM: 具有可控条件的检索模型

【电商搜索】CRM: 具有可控条件的检索模型 目录 文章目录 【电商搜索】CRM: 具有可控条件的检索模型目录文章信息摘要研究背景问题与挑战如何解决核心创新点算法模型实验效果&#xff08;包含重要数据与结论&#xff09;相关工作后续优化方向 后记 https://arxiv.org/pdf/2412.…

【python自动化六】UI自动化基础-selenium的使用

selenium是目前用得比较多的UI自动化测试框架&#xff0c;支持java&#xff0c;python等多种语言&#xff0c;目前我们就选用selenium来做UI自动化。 1.selenium安装 安装命令 pip install selenium2.selenium的简单使用 本文以chrome浏览器为例&#xff0c;配套selenium中c…

Sigrity Optimize PI CapGen仿真教程文件路径

为了方便读者能够快速上手和学会Sigrity Optimize PI和 Deacap Generate 的功能&#xff0c;将Sigrity Optimize PI CapGen仿真教程专栏所有文章对应的实例文件上传至以下路径 https://download.csdn.net/download/weixin_54787054/90171471?spm1001.2014.3001.5503

免费线上签字小程序,开启便捷电子签名

虽如今数字化飞速发展的时代&#xff0c;但线上签名小程序的开发制作却并非易事。需要攻克诸多技术难题&#xff0c;例如确保签名的真实性与唯一性&#xff0c;防止签名被伪造或篡改。 要精准地捕捉用户手写签名的笔迹特征&#xff0c;无论是笔画的粗细、轻重&#xff0c;还是…

02、服务器的分类和开发项目流程

硬件介绍 1、服务器分类2.开发流程 1、服务器分类 1.1 服务器分类 1u服务器&#xff08;u表示服务器的厚度&#xff09; 1U4.45cm&#xff1b; 4u服务器&#xff08;u表示服务器的厚度&#xff09; &#xff0c; 服务器有两个电源模块&#xff0c;接在不同的电源&#xff0c;…

canvas绘制仪表盘刻度盘

canvas画布可以实现在网页上绘制图形的方法&#xff0c;比如图表、图片处理、动画、游戏等。今天我们在vue模板下用canvas实现仪表盘的绘制。 对canvas不熟悉的同学可以先了解下canvas的API文档&#xff1a;canvas API中文网 - Canvas API中文文档首页地图 一、创建模板&#…

搭建Alist(Windows系统环境下的)并挂载阿里云盘open映射到公网

文章目录 前言1. 使用Docker本地部署Alist1.1 本地部署 Alist1.2 访问并设置Alist1.3 在管理界面添加存储 2. 安装cpolar内网穿透 前言 本文将讲解如何在 Windows 系统中借助 Docker 部署 Alist 这一强大的全平台网盘工具&#xff0c;并结合 cpolar 内网穿透&#xff0c;实现随…