本章分享調用騰訊雲API文檔,實現語音合成的技術。
package com.example.combat.controller; import com.example.combat.service.ASRService; import com.example.combat.asrutils.R; import com.example.combat.asrutils.param.CreateRecTask; import com.example.combat.asrutils.param.SentenceRecognition; import com.example.combat.afsutils.Base64ConvertUtils; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** * @description: * @author: zhucj * @date: 2019-11-25 9:30 */ @RestController @RequestMapping("/afs") public class ASRControllerl { @Autowired private ASRService asrService; @ApiOperation(value = "一句話語音識別,60秒時間") @ApiImplicitParams({ @ApiImplicitParam(name = "sourceType",value = "語音數據來源。0:語音 URL;1:語音數據(post body)",required = true,dataType = "Integer"), @ApiImplicitParam(name = "url",value = "語音 URL,公網可下載。當 SourceType 值為 0(語音 URL上傳) 時須填寫該字段",dataType = "String"), @ApiImplicitParam(name = "file",value ="語音數據,當SourceType 值為1(本地語音數據上傳)時必須填寫",dataType = "MultipartFile"), }) @PostMapping(value = "sentence") public R sentenceRecognition(@RequestParam(value = "sourceType") Integer sourceType, @RequestParam(value = "file",required = false)MultipartFile file, @RequestParam(value = "url",required = false) String url){ return asrService.sentenceRecognition(SentenceRecognition .builder() .sourceType(sourceType) .voiceFormat(file==null?url.substring(url.length()-3,url.length()):file.getOriginalFilename().substring(file.getOriginalFilename().length()-3,file.getOriginalFilename().length())) .dataLen(file==null?null:Integer.valueOf(String.valueOf(file.getSize()))) .data(file==null?null: Base64ConvertUtils.getImageStr(file)) .url(url==null?null:url) .build()); } @ApiOperation(value = "錄音文件識別") @ApiImplicitParams({ @ApiImplicitParam(name = "sourceType",value = "語音數據來源。0:語音 URL;1:語音數據(post body)",required = true,dataType = "Integer"), @ApiImplicitParam(name = "url",value = "語音 URL,公網可下載。當 SourceType 值為 0(語音 URL上傳) 時須填寫該字段",dataType = "String"), @ApiImplicitParam(name = "file",value ="語音數據,當SourceType 值為1(本地語音數據上傳)時必須填寫",dataType = "MultipartFile"), }) @PostMapping(value = "createRecTask") public R createRecTask(@RequestParam(value = "sourceType") Integer sourceType, @RequestParam(value = "file",required = false)MultipartFile file, @RequestParam(value = "url",required = false) String url){ return asrService.createRecTask(CreateRecTask .builder() .sourceType(sourceType) .dataLen(file==null?null:Integer.valueOf(String.valueOf(file.getSize()))) .data(file==null?null: Base64ConvertUtils.getImageStr(file)) .url(url==null?null:url) .build()); } }
package com.example.combat.service; import com.example.combat.asrutils.R; import com.example.combat.asrutils.param.CreateRecTask; import com.example.combat.asrutils.param.SentenceRecognition; /** * @description: 語音識別接口 * @author: zhucj * @date: 2019-11-25 9:23 */ public interface ASRService { /** * 一句話識別接口 * @param param * @return */ R sentenceRecognition(SentenceRecognition param); /** * 錄音文件識別 * @param createRecTask * @return */ R createRecTask(CreateRecTask createRecTask); }
package com.example.combat.service.Impl; import com.example.combat.service.ASRService; import com.example.combat.asrutils.ASRUtil; import com.example.combat.asrutils.R; import com.example.combat.asrutils.param.CreateRecTask; import com.example.combat.asrutils.param.SentenceRecognition; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @description: 語音識別實現類 * @author: zhucj * @date: 2019-11-25 9:28 */ @Service public class ASRServiceImpl implements ASRService { @Autowired private ASRUtil asrUtil; @Override public R sentenceRecognition(SentenceRecognition param) { return asrUtil.sentenceRecognition(param); } @Override public R createRecTask(CreateRecTask createRecTask) { return asrUtil.createRecTask(createRecTask); } }
package com.example.combat.asrutils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.example.combat.config.constant.ContentTypeEnum; import com.example.combat.config.constant.HttpMethodEnum; import com.example.combat.config.constant.SignMenodEnum; import com.example.combat.asrutils.param.*; import com.example.combat.afsutils.HttpUtil; import com.example.combat.afsutils.SignUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.*; /** * @description: 語音識別工具類 * @author: zhucj * @date: 2019-11-23 15:30 */ @Component @Slf4j public class ASRUtil { @Value("${tencent.secretId}") private String sercretId; @Value("${tencent.secretKey}") private String sercretKey; /** * 一句話識別 * @param param * @return */ public R sentenceRecognition(SentenceRecognition param){ //獲取公共請求參數 TreeMap treeMap = createPublicMap("SentenceRecognition", "2019-06-14"); HashMap<String,Object> hashMap = new HashMap<>(); hashMap.put("ProjectId",0); hashMap.put("SubServiceType",2); hashMap.put("EngSerViceType","8k"); hashMap.put("SourceType",param.getSourceType()); verifyVoiceFormat(param.getVoiceFormat()); hashMap.put("VoiceFormat",param.getVoiceFormat()); hashMap.put("UsrAudioKey",IDUtil.createIdbyUUID()); if (Objects.equals(0,param.getSourceType())){ if (StringUtils.isEmpty(param.getUrl())){ throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"傳入語音Url類型時,傳入的url不能為空"); } //如果是語音Url,只需要傳Url hashMap.put("Url",param.getUrl()); }else if (Objects.equals(1,param.getSourceType())){ if (StringUtils.isEmpty(param.getData()) ||StringUtils.isEmpty(param.getDataLen()) ){ throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"傳入語音數據類型時,傳入的語音數據和長度不能為空"); } if(param.getDataLen()>614400){ throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"傳入的音頻文件不能超過600kb");} hashMap.put("Data",param.getData()); hashMap.put("DataLen",param.getDataLen()); }else { throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"語音數據來源,傳入的類型錯誤"); } //簽名,公共參數不需要放到body中 String sign = null; try { sign = SignUtils.sign(treeMap, HttpMethodEnum.POST, SignMenodEnum.TC3_HMAC_SHA256, JSON.toJSONString(hashMap) , SentenceRecognitionApi.SENTENCE_RECOGNITION, sercretKey, ContentTypeEnum.JSON); } catch (Exception e) { log.error("簽名異常:{}",e.getMessage()); return R.error("簽名異常").setCode(SystemConstants.SERVER_ERROR_CODE); } try { String respJson = HttpUtil.httpPost(SentenceRecognitionApi.SENTENCE_RECOGNITION, JSON.parseObject(sign,Map.class),hashMap); JSONObject jsonObject = JSON.parseObject(respJson); String response = jsonObject.getString("Response"); JSONObject error =(JSONObject) JSON.parseObject(response).get("Error"); if (Objects.nonNull(error)){ return R.error(String.valueOf(error.get("Message"))).setCode(SystemConstants.SERVER_ERROR_CODE); }else { SentenceResponse sentenceResponse = JSON.parseObject(response, SentenceResponse.class); return R.ok(sentenceResponse.getResult()).setCode(SystemConstants.SUCCESS_CODE); } } catch (Exception e) { log.error("語音識別失敗:{}",e.getMessage()); return R.error("語音識別失敗").setCode(SystemConstants.SERVER_ERROR_CODE); } } /** * 錄音文件識別 * @param createRecTask * @return */ public R createRecTask(CreateRecTask createRecTask){ TreeMap treeMap = createPublicMap("CreateRecTask", "2019-06-14"); HashMap<String,Object> hashMap = new HashMap(); hashMap.put("EngineModelType","8k_0"); hashMap.put("ChannelNum",1); hashMap.put("ResTextFormat",0); hashMap.put("SourceType",createRecTask.getSourceType()); if (Objects.equals(createRecTask.getSourceType(),0)){ hashMap.put("Url",createRecTask.getUrl()); }else if (Objects.equals(createRecTask.getSourceType(),1)){ hashMap.put("Data",createRecTask.getData()); if (createRecTask.getDataLen()>5*1024*1024){ throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"錄音文件不能超過5MB"); } hashMap.put("DataLen",createRecTask.getDataLen()); } //簽名,公共參數不需要放到body中 String sign = null; try { sign = SignUtils.sign(treeMap, HttpMethodEnum.POST, SignMenodEnum.TC3_HMAC_SHA256, JSON.toJSONString(hashMap) , SentenceRecognitionApi.SENTENCE_RECOGNITION, sercretKey, ContentTypeEnum.JSON); } catch (Exception e) { log.error("簽名異常:{}",e.getMessage()); return R.error("簽名異常").setCode(SystemConstants.SERVER_ERROR_CODE); } try { String respJson = HttpUtil.httpPost(SentenceRecognitionApi.SENTENCE_RECOGNITION, JSON.parseObject(sign,Map.class),hashMap); JSONObject jsonObject = JSON.parseObject(respJson); String response = jsonObject.getString("Response"); JSONObject error =(JSONObject) JSON.parseObject(response).get("Error"); if (Objects.nonNull(error)){ return R.error(String.valueOf(error.get("Message"))).setCode(SystemConstants.SERVER_ERROR_CODE); }else { JSONObject data =(JSONObject) JSON.parseObject(response).get("Data"); String taskId =String.valueOf(data.get("TaskId")); //通過TaskId查詢識別內容 return describeTaskStatus(taskId); } } catch (Exception e) { log.error("語音識別失敗:{}",e.getMessage()); return R.error("語音識別失敗").setCode(SystemConstants.SERVER_ERROR_CODE); } } /** * 通過任務ID查詢識別結果 * @param taskId * @return */ public R describeTaskStatus(String taskId){ TreeMap treeMap = createPublicMap("DescribeTaskStatus", "2019-06-14"); HashMap<String,Object> hashMap = new HashMap<>(); hashMap.put("TaskId",Integer.valueOf(taskId)); //簽名,公共參數不需要放到body中 String sign = null; try { sign = SignUtils.sign(treeMap, HttpMethodEnum.POST, SignMenodEnum.TC3_HMAC_SHA256, JSON.toJSONString(hashMap) , SentenceRecognitionApi.SENTENCE_RECOGNITION, sercretKey, ContentTypeEnum.JSON); } catch (Exception e) { log.error("簽名異常:{}",e.getMessage()); return R.error("簽名異常").setCode(SystemConstants.SERVER_ERROR_CODE); } try { String respJson = HttpUtil.httpPost(SentenceRecognitionApi.SENTENCE_RECOGNITION, JSON.parseObject(sign,Map.class),hashMap); JSONObject jsonObject = JSON.parseObject(respJson); String response = jsonObject.getString("Response"); CreateRecTaskResponse createRecTaskResponse= JSON.parseObject(response, CreateRecTaskResponse.class); Data data = createRecTaskResponse.getData(); if (Objects.equals(data.getStatus(),0)){ return describeTaskStatus(taskId); }else if (Objects.equals(data.getStatus(),1)){ return describeTaskStatus(taskId); }else if (Objects.equals(data.getStatus(),2)){ return R.ok(data.getResult()).setCode(SystemConstants.SUCCESS_CODE); }else { return R.error(data.getErrorMsg()).setCode(SystemConstants.SERVER_ERROR_CODE); } } catch (Exception e) { log.error("任務ID查詢識別失敗:{}",e.getMessage()); return R.error("任務ID查詢識別失敗").setCode(SystemConstants.SERVER_ERROR_CODE); } } /** * 封裝請求公共參數 * @param action * @param version * @return */ public TreeMap createPublicMap(String action, String version){ TreeMap<String,Object> treeMap = new TreeMap<>(); treeMap.put("Action",action); treeMap.put("Version",version); treeMap.put("Timestamp",getCurrentTimestamp()); treeMap.put("Nonce",new Random().nextInt(Integer.MAX_VALUE)); treeMap.put("SecretId",sercretId); return treeMap; } /** * 獲取當前時間戳,單位秒 * @return */ public static long getCurrentTimestamp() { return System.currentTimeMillis()/1000; } public void verifyVoiceFormat(String type){ if (Objects.equals("mp3",type) || Objects.equals("wav",type)){ return; }else { throw new ASRRuntimeException(SystemConstants.PARAM_INCORRECT_CODE,"傳入識別音頻的音頻格式錯誤"); } } }
package com.example.combat.asrutils; import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; /** * 名稱:IDUtil <br> * 描述:ID 生成工具類<br> * * @author zhucj * @version 1.0 * @since 1.0.0 */ public class IDUtil { /** * 主要功能:生成流水號 yyyyMMddHHmmssSSS + 3位隨機數 * 注意事項:無 * * @return 流水號 */ public static String createIdByDate() { // 精確到毫秒 SimpleDateFormat fmt = new SimpleDateFormat("(yyyyMMddHHmmssSSS)"); String suffix = fmt.format(new Date()); suffix = suffix + "-" + Math.round((Math.random() * 100000)); return suffix; } /** * 主要功能:生成uuid * 注意事項:無 * * @return uuid 32 位 */ public static String createIdbyUUID() { return UUID.randomUUID().toString().replaceAll("-", ""); } }
package com.example.combat.asrutils; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.ToString; import java.io.Serializable; /** * 返回類型 * @author choleece * @date 2018/9/27 */ @ApiModel @ToString public class R<T> implements Serializable { private static final long serialVersionUID = -6287952131441663819L; /** * 編碼 */ @ApiModelProperty(value = "響應碼", example = "200") private int code = 200; /** * 成功標志 */ @ApiModelProperty(value = "成功標志", example = "true") private Boolean success; /** * 返回消息 */ @ApiModelProperty(value = "返回消息說明", example = "操作成功") private String msg="操作成功"; /** * 返回數據 */ @ApiModelProperty(value = "返回數據") private T data; /** * 創建實例 * @return */ public static R instance() { return new R(); } public int getCode() { return code; } public R setCode(int code) { this.code = code; return this; } public Boolean getSuccess() { return success; } public R setSuccess(Boolean success) { this.success = success; return this; } public String getMsg() { return msg; } public R setMsg(String msg) { this.msg = msg; return this; } public T getData() { return data; } public R setData(T data) { this.data = data; return this; } public static R ok() { return R.instance().setSuccess(true); } public static R ok(Object data) { return ok().setData(data); } public static R ok(Object data, String msg) { return ok(data).setMsg(msg); } public static R error() { return R.instance().setSuccess(false); } public static R error(String msg) { return error().setMsg(msg); } /** * 無參 */ public R() { } public R(int code, String msg) { this.code = code; this.msg = msg; } public R(int code, T data){ this.code = code; this.data = data; } /** * 有全參 * @param code * @param msg * @param data * @param success */ public R(int code, String msg, T data, Boolean success) { this.code = code; this.msg = msg; this.data = data; this.success = success; } /** * 有參 * @param code * @param msg * @param data */ public R(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }
package com.example.combat.asrutils.param; /** * @description: 語音識別異常類 * @author: 朱傳捷 80004071 * @date: 2019-11-23 15:46 */ public class ASRRuntimeException extends RuntimeException{ /** * 錯誤碼 */ private Integer errorCode; /** * 錯誤描述 */ private String errorMsg; public ASRRuntimeException(Integer errorCode, String errorMsg){ super(errorMsg); this.errorCode = errorCode; this.errorMsg = errorMsg; } public Integer getErrorCode() { return errorCode; } public String getErrorMsg() { return errorMsg; } }
package com.example.combat.asrutils.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @description: 錄音文件識別請求參數 * @author: 朱傳捷 80004071 * @date: 2019-11-25 12:22 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class CreateRecTask { /** * 語音數據來源。0:語音 URL;1:語音數據(post body)。 */ private Integer sourceType; /** * 回調 URL,用戶自行搭建的用於接收識別結果的服務器地址, 長度小於2048字節 */ private String CallbackUrl; /** * 語音的URL地址,需要公網可下載當 SourceType 值為 0 時須填寫該字段,為 1 時不需要填寫。 * 注意:請確保錄音文件時長在一個小時之內,否則可能識別失敗。請保證文件的下載速度,否則可能下載失敗。 */ private String url; /** * 語音數據,當SourceType 值為1時必須填寫,為0可不寫。 */ private String data; /** * 數據長度,當 SourceType 值為1時必須填寫,為0可不寫(此數據長度為數據未進行base64編碼時的數據長度)。 */ private Integer dataLen; }
package com.example.combat.asrutils.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @description: 錄音識別返回結果 * @author: 朱傳捷 80004071 * @date: 2019-11-25 14:05 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class CreateRecTaskResponse extends Response { private com.example.combat.asrutils.param.Data Data; }
package com.example.combat.asrutils.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.NoArgsConstructor; /** * @description: * @author: 朱傳捷 80004071 * @date: 2019-11-25 14:18 */ @lombok.Data @AllArgsConstructor @NoArgsConstructor @Builder public class Data { private Integer TaskId; private Integer Status; private String StatusStr; private String Result; private String ErrorMsg; }
package com.example.combat.asrutils.param; import lombok.Data; /** * @description: * @author: 朱傳捷 80004071 * @date: 2019-10-18 15:58 */ @Data public class Error { private String Code; private String Messag; }
package com.example.combat.asrutils.param; import lombok.Data; /** * @description: 請求返回公共參數 * @author: 朱傳捷 80004071 * @date: 2019-10-18 15:56 */ @Data public class Response { private String RequestId; private Error Error; }
package com.example.combat.asrutils.param; import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotNull; /** * @description: 語音識別實體 * @author: 朱傳捷 80004071 * @date: 2019-11-23 15:21 */ @Data @Builder public class SentenceRecognition { /** * 語音數據來源。0:語音 URL;1:語音數據(post body)。 */ @NotNull(message = "語音數據來源不能為空") private Integer sourceType; /** * 識別音頻的音頻格式。mp3、wav。 */ @NotNull(message = "識別音頻的音頻格式不為空") private String voiceFormat; /** * 語音 URL,公網可下載。當 SourceType 值為 0(語音 URL上傳) 時須填寫該字段, * 為 1 時不填;URL 的長度大於 0,小於 2048,需進行urlencode編碼。音頻時間長度要小於60s。 */ private String url; /** * 語音數據,當SourceType 值為1(本地語音數據上傳)時必須填寫,當SourceType 值為0(語音 URL上傳)可不寫。 * 要使用base64編碼(采用python語言時注意讀取文件應該為string而不是byte, * 以byte格式讀取后要decode()。編碼后的數據不可帶有回車換行符)。音頻數據要小於600KB。 */ private String data; /** * 數據長度,單位為字節。當 SourceType 值為1(本地語音數據上傳)時必須填寫,當 SourceType 值為0 * (語音 URL上傳)可不寫(此數據長度為數據未進行base64編碼時的數據長度)。 */ private Integer dataLen; }
package com.example.combat.asrutils.param; /** * @description: 語音識別API * @author: 朱傳捷 80004071 * @date: 2019-11-23 16:07 */ public class SentenceRecognitionApi { /** * 語音識別 */ public static final String SENTENCE_RECOGNITION = "https://asr.ap-shenzhen-fsi.tencentcloudapi.com"; }
package com.example.combat.asrutils.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @description: 返回響應參數 * @author: 朱傳捷 80004071 * @date: 2019-11-23 15:32 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SentenceResponse extends Response { /** * 識別結果 */ private String Result; }
package com.example.combat.asrutils.param; /** * @ClassName SystemConstants * @Description 常量字段 * @Author YangLei * @Date 2019/5/7 11:13 * @Version 1.0 **/ public class SystemConstants { /** 傳參不規范,code:400*/ public static final Integer PARAM_INCORRECT_CODE = 400; /** 成功,code:200*/ public static final Integer SUCCESS_CODE = 200; /** 服務內部調用失敗,code:500*/ public static final Integer SERVER_ERROR_CODE = 500; /** 登錄失效,code:401*/ public static final Integer AUTH_FAIL_CODE = 401; /** 無對應接口權限,code:402*/ public static final Integer HAVE_NOT_PERMISSION_CODE = 402; /** 操作無記錄,code:403*/ public static final Integer NO_RECORD_OPERATION = 403; //http請求方式 public static final String HTTP_METHOD_POST = "POST"; public static final String HTTP_METHOD_GET = "GET"; public static final String HTTP_METHOD_PUT = "PUT"; public static final String HTTP_METHOD_DELETE = "DELETE"; }
#騰訊雲服務配置 tencent: secretId: ********* secretKey: ********
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox- swagger2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>
<!--httpclient -->
<dependency> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>