微信小程式語音同步智慧識別的實現案例

智慧zhuhuix發表於2020-05-29

一、背景

在小程式的一些應用場景中,會有語音轉文字的需求。原有的做法一般是先通過小程式的錄音功能錄下語音檔案,然後再通過呼叫語音智慧識別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
在這裡插入圖片描述

五、實際測試

語音測試正常
在這裡插入圖片描述
上傳檔案至後臺:
在這裡插入圖片描述
上傳的日誌資訊檢視:
在這裡插入圖片描述

相關文章