一、背景
在小程式的一些應用場景中,會有語音轉文字的需求。原有的做法一般是先通過小程式的錄音功能錄下語音檔案,然後再通過呼叫語音智慧識別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
五、實際測試
語音測試正常
上傳檔案至後臺:
上傳的日誌資訊檢視: