微信小程序语音同步智能识别的实现案例
一、背景
在小程序的一些应用场景中,会有语音转文字的需求。原有的做法一般是先通过小程序的录音功能录下语音文件,然后再通过调用语音智能识别WebApi(比如百度云AI平台,科大讯飞平台)将语音文件转成文字信息,以上的做法比较繁琐且用户的体验性较差。
为解决此问题,微信直接开放了同声传译的插件,小程序作者可以直接使用该插件进行语音同声传译的开发。此文章将通过前后端整合应用的完整案例完成语音的实时转换,并将语音上传到服务端后台备份。
二、同声传译插件介绍
微信同声传译由微信智聆语音团队、微信翻译团队与公众平台联合推出的同传开放接口,首期开放语音转文字、文本翻译、语音合成接口,为开发者赋能。
1、 微信小程序后台添加插件
进入微信小程序后台-->进入设置-->第三方设置-->添加插件->搜索同声传译-->完成添加。
2、 微信小程序启用插件
在小程序app.json文件中增加插件版本等信息:
"plugins": { "WechatSI": { "version": "0.3.3", "provider": "wx069ba97219f66d99" } },
在页面程序文件中引入插件:
/* index.js */ const plugin = requirePlugin("WechatSI") // 获取**全局唯一**的语音识别管理器**recordRecoManager** const manager = plugin.getRecordRecognitionManager()
recordRecoManager 对象的方法列表:
方法 | 参数 | 说明 |
---|---|---|
start | options | 开始识别 |
stop | 结束识别 | |
onStart | callback | 正常开始录音识别时会调用此事件 |
onRecognize | callback | 有新的识别内容返回,则会调用此事件 |
onStop | callback | 识别结束事件 |
onError | callback | 识别错误事件 |
官方开发文档:插件的语音识别管理器
三、语音同步转换的前端实现
1、界面UI与操作
UI参考微信官方的DEMO:长按按钮进行录音,松开按钮实时将录音转换为文字。
用户可对同步转换的文字进行编辑,同时可将原始语音文件与文字上传后台服务端。
2、代码实现
语音同步转换的主要代码:
//导入插件 const plugin = requirePlugin("WechatSI"); // 获取**全局唯一**的语音识别管理器**recordRecoManager** const manager = plugin.getRecordRecognitionManager(); /** * 加载进行初始化 */ onLoad: function () { //获取录音权限 app.getRecordAuth(); //初始化语音识别回调 this.initRecord(); }, ... /** * 初始化语音识别回调 * 绑定语音播放开始事件 */ initRecord: function () { //有新的识别内容返回,则会调用此事件 manager.onRecognize = (res) => { let currentData = Object.assign({}, this.data.currentTranslate, { text: res.result, }); this.setData({ currentTranslate: currentData, }); this.scrollToNew(); }; // 识别结束事件 manager.onStop = (res) => { let text = res.result; console.log(res.tempFilePath); if (text == "") { this.showRecordEmptyTip(); return; } let lastId = this.data.lastId + 1; let currentData = Object.assign({}, this.data.currentTranslate, { text: res.result, translateText: "正在识别中", id: lastId, voicePath: res.tempFilePath, duration: res.duration }); this.setData({ currentTranslate: currentData, recordStatus: 1, lastId: lastId, }); //将当前识别内容与语音文件加入列表 this.addRecordFile(currentData, this.data.dialogList.length); //刷新列表 this.scrollToNew(); }; // 识别错误事件 manager.onError = (res) => { this.setData({ recording: false, bottomButtonDisabled: false, }); }; }, /** * 按住按钮开始语音识别 */ streamRecord: function (e) { let detail = e.detail || {}; let buttonItem = detail.buttonItem || {}; //开始中文录音 manager.start({ lang: buttonItem.lang, }); this.setData({ recordStatus: 0, recording: true, currentTranslate: { // 当前语音输入内容 create: util.recordTime(new Date()), text: "正在聆听中", lfrom: buttonItem.lang, lto: buttonItem.lto, }, }); //刷新列表 this.scrollToNew(); }, /** * 松开按钮结束语音识别 */ streamRecordEnd: function (e) { let detail = e.detail || {}; // 自定义组件触发事件时提供的detail对象 let buttonItem = detail.buttonItem || {}; // 防止重复触发stop函数 if (!this.data.recording || this.data.recordStatus != 0) { console.warn("has finished!"); return; } manager.stop(); this.setData({ bottomButtonDisabled: true, }); },
编辑识别文字并完上传的主要代码:
/** * 页面的初始数据 */ data: { edit_text_max: 200, remain_length: 200, edit_text: "", is_focus: false, tips: "", index: -1, voicePath: "", }, /** * 加载初始化 */ onLoad: function (options) { //根据传入的文字内容填充编辑框 this.setEditText(options.content) this.setData({ index: index, oldText:options.content, voicePath: options.voicePath }) }, /** * 编辑文字 */ editInput: function (event) { console.log(event) if (event.detail.value.length > this.getEditTextMax()) { } else { this.data.edit_text = event.detail.value this.updateRemainLength(this.data.edit_text) } }, /** * 上传文字与语音文件 */ editConfirm: function (event) { let json=this.data.edit_text //调用微信上传文件api将信息上传至服务端webApi wx.uploadFile({ url: api.wxFileUploadUrl, filePath: this.data.voicePath, name: "file", header: { Authorization: wx.getStorageSync("loginFlag"), "Content-Type": "multipart/form-data", }, formData: { openId: app.globalData.userInfo.openId, realName: "语音文件", json: JSON.stringify(json), }, success: (result) => { console.log("success:", result); if (result.statusCode == "200") { let data = JSON.parse(result.data); console.log("data", data); if (data.success == true) { let module = data.module; console.log("module", module); app.showInfo("上传成功"); setTimeout( ()=>{ wx.navigateBack(); }, 2000) } else { app.showInfo("异常错误" + data.errMsg + ",请重新进入"); wx.navigateTo({ url: "/pages/index/index", }); } } else { app.showInfo("访问后台异常,重新进入系统"); wx.navigateTo({ url: "/pages/index/index", }); } }, fail: (result) => { console.log("fail", result); wx.navigateTo({ url: "/pages/index/index", }); }, complete: () => {}, }); },
四、后端SpringBoot实现语音文件上传webApi
1、SpringBoot项目API相关结构树
2、文件上传工具类的实现
tools工具类包中主要存文件通用的文件上传工具类,该工具类会将文件上传至配置指定的文件夹下,并将文件信息写入upload_file表中。
- 文件信息实体类:与数据库中表upload_file对应;
- 文件存储仓库类:通过Spring Data JPA接口实现数据的CRUD;
- 文件上传工具接口:对外统一封装文件上传方法;
- 文件上传工具实现类:实现文件上传方法接口。
文件信息实体类:UploadFile.java
/** * 文件信息表 * * @author zhuhuix * @date 2020-04-20 */ @Entity @Getter @Setter @Table(name = "upload_file") public class UploadFile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @NotNull(groups = Update.class) private Long id; /** * 文件实际名称 */ @Column(name = "real_name") private String realName; /** * 文件名 */ @NotNull @Column(name = "file_name") private String fileName; /** * 文件主名称 */ @NotNull @Column(name = "primary_name") private String primaryName; /** * 文件扩展名 */ @NotNull private String extension; /** * 存放路径 */ @NotNull private String path; /** * 文件类型 */ private String type; /** * 文件大小 */ private Long size; /** * 上传人 */ private String uploader; @JsonIgnore @Column(name = "create_time") @CreationTimestamp private Timestamp createTime; public UploadFile(String realName, @NotNull String fileName, @NotNull String primaryName, @NotNull String extension, @NotNull String path, String type, Long size, String uploader) { this.realName = realName; this.fileName = fileName; this.primaryName = primaryName; this.extension = extension; this.path = path; this.type = type; this.size = size; this.uploader = uploader; } @Override public String toString() { return "UploadFile{" + "fileName=‘" + fileName + ‘\‘‘ + ", uploader=‘" + uploader + ‘\‘‘ + ", createTime=" + createTime + ‘}‘; } }
文件存储仓库类:UploadFileRepository.java
/** * 上传文件DAO接口层 * * @author zhuhuix * @date 2020-04-03 */ public interface UploadFileRepository extends JpaRepository<UploadFile, Long>, JpaSpecificationExecutor<UploadFile> { //该接口继承JpaRepository及CrudRepository接口,已实现了如findById,save,delete等CRUD方法 }
UploadFileRepository 接口继承JpaRepository及CrudRepository接口,已实现了如findById,save,delete等CRUD方法
文件上传工具接口:UploadFileTool.java
/** * 文件上传接口定义 * * @author zhuhuix * @date 2020-04-20 */ public interface UploadFileTool { /** * 文件上传 * @param multipartFile 文件 * @return 上传信息 */ UploadFile upload(String uploader,String realName,MultipartFile multipartFile); }
文件上传工具实现类:UploadFileToolImpl.java
/** * 文件上传实现类 * * @author zhuhuix * @date 2020-04-20 */ @Service @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class) public class UploadFileToolImpl implements UploadFileTool { private final UploadFileRepository uploadFileRepository; @Value("${uploadFile.path}") private String path; @Value("${uploadFile.maxSize}") private long maxSize; public UploadFileToolImpl(UploadFileRepository uploadFileRepository) { this.uploadFileRepository = uploadFileRepository; } @Override @Transactional(rollbackFor = Exception.class) public UploadFile upload(String uploader, String realName, MultipartFile multipartFile) { //检查文件大小 if (multipartFile.getSize() > maxSize * Constant.MB) { throw new RuntimeException("超出文件上传大小限制" + maxSize + "MB"); } //获取上传文件的主文件名与扩展名 String primaryName = FileUtil.mainName(multipartFile.getOriginalFilename()); String extension = FileUtil.extName(multipartFile.getOriginalFilename()); //根据文件扩展名得到文件类型 String type = getFileType(extension); //给上传的文件加上时间戳 LocalDateTime date = LocalDateTime.now(); DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMddhhmmssS"); String nowStr = "-" + date.format(format); String fileName = primaryName + nowStr + "." + extension; try { String filePath = path + type + File.separator + fileName; File dest = new File(filePath).getCanonicalFile(); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } multipartFile.transferTo(dest); if (ObjectUtil.isNull(dest)) { throw new RuntimeException("上传文件失败"); } UploadFile uploadFile = new UploadFile(realName, fileName, primaryName, extension, dest.getPath(), type, multipartFile.getSize(), uploader); return uploadFileRepository.save(uploadFile); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } } /** * 根据文件扩展名给文件类型 * * @param extension 文件扩展名 * @return 文件类型 */ private static String getFileType(String extension) { String document = "txt doc pdf ppt pps xlsx xls docx csv"; String music = "mp3 wav wma mpa ram ra aac aif m4a"; String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; if (image.contains(extension)) { return "image"; } else if (document.contains(extension)) { return "document"; } else if (music.contains(extension)) { return "music"; } else if (video.contains(extension)) { return "video"; } else { return "other"; } } }
注意,该程序代码中用到了@Value注解获取配置文件中的uploadFile.path及uploadFile.maxsize参数,一般在项目静态配置文件中按如下书写(yml配置文件)。
# 测试环境文件存储路径 uploadFile: path: C:\startup\file # 文件大小 /M maxSize: 50
3、小程序上传文件接口的实现
wx-miniprogram包定义了小程序CRM webApi的接口,小程序调用webApi实现文件的上传及其他功能。
- 微信小程序 webApi:对外提供小程序上传文件webApi;
- 微信小程序服务接口:封装小程序上传文件服务接口;
- 微信小程序服务实现:小程序上传文件服务的实现,该服务实现中会调用tools包中的UploadFile接口进行文件的上传。
微信小程序CRM webApi:WxMiniCrmController.java
/** * 微信小程序Crm webApi * * @author zhuhuix * @date 2020-03-30 */ @Slf4j @RestController @RequestMapping("/api/wx-mini") @Api(tags = "微信小程序Crm接口") public class WxMiniCrmController { private final WxMiniCrm wxMiniCrm; public WxMiniCrmController(WxMiniCrm wxMiniCrm) { this.wxMiniCrm = wxMiniCrm; } @ApiOperation(value = "微信小程序端上传文件") @PostMapping(value = "/fileUpload") public ResponseEntity fileUpload(HttpServletRequest request) { MultipartHttpServletRequest req = (MultipartHttpServletRequest) request; MultipartFile multipartFile = req.getFile("file"); String openId = req.getParameter("openId"); String realName = req.getParameter("realName"); String json = req.getParameter("json"); return ResponseEntity.ok(wxMiniCrm.uploadFile(json, openId,realName, multipartFile)); } }
微信小程序CRM服务接口:WxMiniCrm.java
/** * 微信小程序CRM服务接口定义 * * @author zhuhuix * @date 2020-04-20 */ public interface WxMiniCrm { /** * 将微信小程序传入的json对象写入数据库,并同时将文件上传至服务端 * * @param json 微信端传入json对象 * @param openId 上传人 * @param realName 文件实际名称 * @param multipartFile 上传文件 * @return 返回上传信息 */ Result<UploadFile> uploadFile(String json, String openId, String realName,MultipartFile multipartFile); }
微信小程序CRM服务实现:WxMiniCrmImpl.java
/** * 微信小程序CRM实现类 * * @author zhuhuix * @date 2020-04-20 */ @Slf4j @Service @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class) public class WxMiniCrmImpl implements WxMiniCrm { private final UploadFileTool uploadFileTool; public WxMiniCrmImpl(UploadFileTool uploadFileTool) { this.uploadFileTool = uploadFileTool; } @Override @Transactional(rollbackFor = Exception.class) public Result<UploadFile> uploadFile(String json, String openId,String realName, MultipartFile multipartFile) { return new Result<UploadFile>().ok(uploadFileTool.upload(openId,realName, multipartFile)); } }
4、小程序上传文件接口的查看
访问Swagger2可查看该接口,Swagger2与SpringBoot的集成可参考SpringBoot JWT认证机制项目集成Swagger2
五、实际测试
语音测试正常
上传文件至后台:
上传的日志信息查看: