30分鐘實現小程式語音識別

黑金團隊發表於2018-11-24

前言

為了參加某個作秀活動,研究了一波如何結合小程式、科大訊飛實現語音錄入、識別的實現。科大訊飛開發文件中只給出 Python 的 demo,並沒有給出 node.js 的 sdk,但問題不大。本文將從小程式相關程式碼到最後對接科大訊飛 api 過程,一步步介紹,半個小時,搭建完成小程式語音識別功能!不能再多了!當然,前提是最好掌握有一點點小程式、node.js 甚至是音訊相關的知識。

架構先行

架構比較簡單,大夥兒可以先看下圖。除了小程式,需要提供 3 個服務,檔案上傳、音訊編碼及對接科大訊飛的服務。

node.js 對接科大訊飛的 api,npm 上已經有同學提供了 sdk,有興趣的同學可以去搜尋瞭解一下,筆者這裡是直接呼叫了科大訊飛的 api 介面。

架構圖

擼起袖子加油幹

1、建立小程式

鵝廠的小程式文件非常詳細,在這裡筆者就不對如何建立一個小程式的步驟進行詳細闡述了。有需要的同學可以檢視鵝廠的小程式開發文件

1.1 相關程式碼

我們摘取小程式裡面,語音錄入和語音上傳部分的程式碼。

// 根據wx提供的api建立錄音管理物件
const recorderManager = wx.getRecorderManager();

// 監聽語音識別結束後的行為
recorderManager.onStop(recorderResponse => {
    // tempFilePath 是錄製的音訊檔案
    const { tempFilePath } = recorderResponse;

    // 上傳音訊檔案,完成語音識別翻譯
    wx.uploadFile({
        url: 'http://127.0.0.1:7001/voice', // 該服務在後面搭建。另外,小程式釋出時要求後臺服務提供https服務!這裡的地址僅為開發環境配置。
        filePath: tempFilePath,
        name: 'file',
        complete: res => {
            console.log(res); // 我們期待res,就是翻譯後的內容
        }
    });
});

// 開始錄音,觸發條件可以是按鈕或其他,由你自己決定
recorderManager.start({
    duration: 5000 // 最長錄製時間
    // 其他引數可以預設,更多引數可以檢視https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.start.html
});
複製程式碼

2、搭建檔案伺服器

步驟 1 程式碼中提到了一個 url 地址大家應該都還記得。

http://127.0.0.1:7001/voice
複製程式碼

小程式本身還並沒有提供語音識別的功能,所以在這裡我們需要藉助於“後端”服務的能力,完成我們語音識別翻譯的功能。

2.1 egg.js 服務初始化

我們使用 egg.js 的 cli 快速初始化一個工程,當然你也可以使用 express、koa、kraken 等等框架,框架的選型在此不是重點我們就不做展開闡述了。對 egg.js 不熟悉的同學可以檢視egg.js 的官網

npm i egg-init -g
egg-init voice-server --type=simple
cd voice-server
npm i
複製程式碼

安裝完成後,執行以下程式碼

npm run dev
複製程式碼

隨後訪問瀏覽器http://127.0.0.1:7001應該可以看到一個Hi, egg 的頁面。至此我們的服務初始化完成。

2.2 檔案上傳介面

a) 修改 egg.js 的檔案上傳配置

開啟 config/config.default.js,新增以下兩項配置

module.exports = appInfo => {
    ...
    config.multipart = {
        fileSize: '2gb', // 限制檔案大小
        whitelist: [ '.aac', '.m4a', '.mp3' ], // 支援上傳的檔案字尾名
    };

    config.security = {
        csrf: {
            enable: false // 關閉csrf
        }
    };
    ...
}

複製程式碼

b) 新增 VoiceController

開啟 app/controller 資料夾,新建檔案 voice.js。編寫 VoiceController 使其繼承於 egg.js 的 Controller。具體程式碼如下:

const Controller = require('egg').Controller;
const fs = require('fs');
const path = require('path');
const pump = require('mz-modules/pump');
const uuidv1 = require('uuid/v1'); // 依賴於uuid庫,用於生成唯一檔名,使用npm i uuid安裝即可

// 音訊檔案上傳後儲存的路徑
const targetPath = path.resolve(__dirname, '..', '..', 'uploads');

class VoiceController extends Controller {
    constructor(params) {
        super(params);
        if (!fs.existsSync(targetPath)) {
            fs.mkdirSync(targetPath);
        }
    }

    async translate() {
        const parts = this.ctx.multipart({ autoFields: true });
        let stream;
        const voicePath = path.join(targetPath, uuidv1());
        while (!isEmpty((stream = await parts()))) {
            await pump(stream, fs.createWriteStream(voicePath));
        }
        // 到這裡就完成了檔案上傳。如果你不需要檔案落地,也可以在後續的操作中,直接使用stream操作檔案流

        ...
        // 音訊編碼
        // 科大訊飛語音識別
        ...
    }
}
複製程式碼

c) 最後一步,新增路由規則

寫完 controller 之後,我們依據 egg.js 的規則,在 router.js 裡面新增一個路由。

module.exports = app => {
    const { router, controller } = app;
    router.get('/', controller.home.index);
    router.get('/voice', controller.voice.translate);
};
複製程式碼

OK,至此你可以測試一下從小程式錄音,錄音完成後上傳到後臺檔案伺服器的完整流程。如果沒問題,那恭喜你你已經完成了 80%的工作了!

3、音訊編碼服務

在上文中,小程式錄音的方法 recorderManager.start 的時候我們提及到了“更多引數”。其中有一個引數是 format,支援 aac 和 mp3 兩種(預設是 aac)。然後我們查閱了科大訊飛的 api 文件,音訊編碼支援“未壓縮的 pcm 或 wav 格式”。

什麼 aac、pcm、wav?emmm.. OK,我們只是前端,既然格式不對等,那隻需要完成 aac -> pcm 轉化即可,ffmpeg 立即浮現在筆者的腦海裡。一番搜尋,命令大概是這樣子的:

ffmpeg -i uploads/a3f588d0-edf8-11e8-b6f5-2929aef1b7f8.aac -f s16le -ar 8000 -ac 2 -y decoded.pcm

# -i 後面帶的是原始檔
# -f s16le 指的是編碼格式
# -ar 8000 編碼位元速率
# -ac 2 通道

複製程式碼

接下來我們使用 node.js 來實現上述命令。

3.1 引入相關依賴包

npm i ffmpeg-static
npm i fluent-ffmpeg
複製程式碼

3.2 建立一個編碼服務

在 app/service 資料夾中,建立 ffmpeg.js 檔案。新建 FFmpegService 繼承於 egg.js 的 Service

const { Service } = require('egg');
const ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
const path = require('path');
const fs = require('fs');

ffmpeg.setFfmpegPath(ffmpegStatic.path);

class FFmpegService extends Service {
    async aac2pcm(voicePath) {
        const command = ffmpeg(voicePath);

        // 方便測試,我們將轉碼後檔案落地到磁碟
        const targetDir = path.join(path.dirname(voicePath), 'pcm');
        if (!fs.existsSync(targetDir)) {
            fs.mkdirSync(targetDir);
        }

        const target = path.join(targetDir, path.basename(voicePath)) + '.pcm';
        return new Promise((resolve, reject) => {
            command
                .audioCodec('pcm_s16le')
                .audioChannels(2)
                .audioBitrate(8000)
                .output(target)
                .on('error', error => {
                    reject(error);
                })
                .on('end', () => {
                    resolve(target);
                })
                .run();
        });
    }
}

module.exports = FFmpegService;
複製程式碼

3.3 呼叫 ffmpegService,獲得 pcm 檔案

回到 app/controller/voice.js 檔案中,我們在檔案上傳完成後,呼叫 ffmpegService 提供的 aac2pcm 方法,獲取到 pcm 檔案的路徑。

// app/controller/voice.js
...
async translate() {
    ...
    ...
    const pcmPath = await this.ctx.service.ffmpeg.aac2pcm(voicePath);
    ...
}
...
複製程式碼

4、對接科大訊飛 API

首先,需要到科大訊飛開放平臺註冊並新增應用、開通應用的語音聽寫服務。

我們再寫一個服務,在 app/service 資料夾下建立 xfyun.js 檔案,實現 XFYunService 繼承於 egg.js 的 Service。

4.1 引入相關依賴

npm i axios // 網路請求庫
npm i md5 // 科大訊飛介面中需要md5計算
npm i form-urlencoded // 介面中需要對部分內容進行urlencoded
複製程式碼

4.2 XFYunService 實現

const { Service } = require('egg');
const fs = require('fs');
const formUrlencoded = require('form-urlencoded').default;
const axios = require('axios');
const md5 = require('md5');
const API_KEY = 'xxxx'; // 在科大訊飛控制檯上可以查到服務的APIKey
const API_ID = 'xxxxx'; // 同樣可以在控制檯查到

class XFYunService extends Service {
    async voiceTranslate(voicePath) {
        // 繼上文,暴力的讀取檔案
        let data = fs.readFileSync(voicePath);
        // 將內容進行base64編碼
        data = new Buffer(data).toString('base64');
        // 進行url encode
        data = formUrlencoded({ audio: data });
        const params = {
            engine_type: 'sms16k',
            aue: 'raw'
        };
        const x_CurTime = Math.floor(new Date().getTime() / 1000) + '',
            x_Param = new Buffer(JSON.stringify(params)).toString('base64');
        return axios({
            url: 'http://api.xfyun.cn/v1/service/v1/iat',
            method: 'POST',
            data,
            headers: {
                'X-Appid': API_ID,
                'X-CurTime': x_CurTime,
                'X-Param': x_Param,
                'X-CheckSum': md5(API_KEY + x_CurTime + x_Param)
            }
        }).then(res => {
            // 查詢成功後,返回response的data
            return res.data || {};
        });
    }
}

module.exports = XFYunService;
複製程式碼

4.3 呼叫 XFYunService,完成語音識別

再次回到 app/controller/voice.js 檔案中,我們在 ffmpeg 轉碼完成後,呼叫 XFYunService 提供的 voiceTranslate 方法,完成語音識別。

// app/controller/voice.js
...
async translate() {
    ...
    ...
    const result = await this.ctx.service.xfyun.voiceTranslate(pcmPath);
    this.ctx.body = result;
    if (+result.code !== 0) {
      this.ctx.status = 500;
    }
}
...
複製程式碼

至此我們完成語音識別的程式碼編寫。主要流程其實很簡單,通過小程式錄入語音檔案,上傳到檔案伺服器之後,通過 ffmpeg 獲取到 pcm 檔案, 最後再轉發到科大訊飛的 api 介面進行識別。

附上專案程式碼:speech-recognizer

以上,如有錯漏,歡迎指正!

@Author: _Jay

相關文章