支援語音識別、自然語言理解的微信小程式(“遙知之”智慧小祕)完整原始碼分享

CopperDong發表於2018-02-25

記錄自己搭建https的silk錄音檔案語音識別服務的呼叫過程,所有程式碼可在文中找連結打包下載

>>>>>>>>>>>>>>>>>>>>>>>> 歡迎轉載 <<<<<<<<<<<<<<<<<<<<<<<<

本文原地址:http://blog.csdn.net/happycxz/article/details/78024986

“遙知之”微信小程式完整原始碼下載:

碼雲:http://git.oschina.net/happycxz/nlp_olami_yaozhizhi_wechat_littleapp_demo

github: https://github.com/happycxz/nlp_olami_yaozhizhi_wechat_littleapp_demo

與本小程式密切相關的幾個文章:

====本文中提到的silk轉wav服務端https的API搭建過程詳解見==== 
微信小程式語音識別服務搭建全過程解析(內附免費的供小程式語音識別的https服務)

微信小程式——智慧小祕“遙知之”原始碼分享(語義理解基於olami)(注:這個是原來不支援語音識別的版本)

這次的改動是基於原來的“遙知之”版本v0.2基礎之上的。加上了語音識別,介面變化較大 。下面主要介紹一下新版本首頁面的功能和程式碼實現。

實現功能

實現一個智慧生活資訊查詢的小祕書功能,支援查天氣、新聞、日曆、匯率、笑話、故事、百科、詩詞、郵編、區號、菜譜、股票、節目預告,還支援閒聊、算24點、數學計算、單位換算、購物、搜尋等功能。

使用方式:

新版上線支援語音識別,按下說話,鬆開傳送。

老版本上支援搖一搖、點介面按鈕、手動輸入、下拉重新整理這四種方式。

掃碼試用(左右皆可)

小程式碼小 二維碼小

介面展示

新版本介面展示

新版本介面展示2

開發資源

  1. 免費開放語義介面平臺 olami.ai
  2. 微信小程式平臺
  3. js, css
  4. 我自己搭建的https的語音識別API介面

原始碼分析

這裡主要介紹新版本首頁相關的程式碼,其它部分程式碼在 微信小程式——智慧小祕“遙知之”原始碼分享(語義理解基於olami)(注:這個是原來不支援語音識別的版本) 的基礎上,變化不怎麼大,具體可參考那篇文章。

asr.js原始碼:

/**
 * 作者:happycxz
 * 時間:2017.09.19
 * 原始碼分享連結:http://blog.csdn.net/happycxz/article/details/78024986
 * 
 * https的silk語音識別API(專供微信小程式呼叫):https://api.happycxz.com/wxapp/silk2asr
 * 該API服務搭建全過程解析及原始碼分享貼:http://blog.csdn.net/happycxz/article/details/78016299
 * 需要使用此API請聯絡作者QQ:404499164
 * 
 * 遵循開放、分享、自由、免費的精神,把開源堅持到底
 */

//獲取應用例項 
var app = getApp()

var UTIL = require('../../utils/util.js');
var GUID = require('../../utils/GUID.js');
var NLI = require('../../utils/NLI.js');

const appkey = require('../../config').appkey
const appsecret = require('../../config').appsecret

//彈幕定時器
var timer;

var pageSelf = undefined;

var doommList = [];
class Doomm {
  constructor() {
    this.text = UTIL.getRandomItem(app.globalData.corpus);
    this.top = Math.ceil(Math.random() * 40);
    this.time = Math.ceil(Math.random() * 8 + 6);
    this.color = getRandomColor();
    this.display = true;
    let that = this;
    setTimeout(function () {
      doommList.splice(doommList.indexOf(that), 1);
      doommList.push(new Doomm());

      pageSelf.setData({
        doommData: doommList
      })
    }, this.time * 1000)
  }
}
function getRandomColor() {
  let rgb = []
  for (let i = 0; i < 3; ++i) {
    let color = Math.floor(Math.random() * 256).toString(16)
    color = color.length == 1 ? '0' + color : color
    rgb.push(color)
  }
  return '#' + rgb.join('')
}

Page({
  data: {
    j: 1,//幀動畫初始圖片 
    isSpeaking: false,//是否正在說話
    outputTxt : "", //輸出識別結果

    doommData: []
  },

  initDoomm: function () {
    doommList.push(new Doomm());
    doommList.push(new Doomm());
    doommList.push(new Doomm());
    this.setData({
      doommData: doommList
    })
  },

  onLoad: function () {
    pageSelf = this;
    this.initDoomm();
  },

  //手指按下 
  touchdown: function () {
    UTIL.log("手指按下了... new date : " + new Date)
    var _this = this;
    speaking.call(this);
    this.setData({
      isSpeaking: true
    })
    //開始錄音 
    wx.startRecord({
      success: function (res) {
        //臨時路徑,下次進入小程式時無法正常使用
        var tempFilePath = res.tempFilePath;
        UTIL.log('record SUCCESS file path:' + tempFilePath)
        _this.setData({
          recordPath: tempFilePath
        });
      },
      fail: function (res) {
        //錄音失敗 
        wx.showModal({
          title: '提示',
          content: '錄音的姿勢不對!',
          showCancel: false,
          success: function (res) {
            if (res.confirm) {
              UTIL.log('使用者點選確定')
              return
            }
          }
        })
      }
    })
  },
  //手指抬起 
  touchup: function () {
    UTIL.log("手指抬起了...")
    this.setData({
      isSpeaking: false,
    })
    clearInterval(this.timer)
    wx.stopRecord()

    var _this = this
    setTimeout(function () {
      var urls = "https://api.happycxz.com/wxapp/silk2asr";
      UTIL.log(_this.data.recordPath);
      wx.uploadFile({
        url: urls,
        filePath: _this.data.recordPath,
        name: 'file',
        formData: { "appKey": appkey, "appSecret": appsecret, "userId": UTIL.getUserUnique() },
        header: { 'content-type': 'multipart/form-data' },
        success: function (res) {
          UTIL.log('res.data:' + res.data);

          var nliResult = getNliFromResult(res.data);
          UTIL.log('nliResult:' + nliResult);
          var stt = getSttFromResult(res.data);
          UTIL.log('stt:' + stt);

          var sentenceResult;
          try {
            sentenceResult = NLI.getSentenceFromNliResult(nliResult);
          } catch (e) {
            UTIL.log('touchup() 錯誤' + e.message + '發生在' + e.lineNumber + '行');
            sentenceResult = '沒明白你說的,換個話題?'
          }

          var lastOutput = "==>語音識別結果:\n" + stt + "\n\n==>語義處理結果:\n" + sentenceResult;
          _this.setData({
            outputTxt: lastOutput,
          });
          wx.hideToast();
        },
        fail: function (res) {
          UTIL.log(res);
          wx.showModal({
            title: '提示',
            content: "網路請求失敗,請確保網路是否正常",
            showCancel: false,
            success: function (res) {
            }
          });
          wx.hideToast();
        }
      });
    }, 1000)
  },

  //切換到老版本
  turnToOld: function() {
    wx.navigateTo({
      url: '../index/index',
    })
  },

})

function getNliFromResult(res_data) {
  var res_data_json = JSON.parse(res_data);
  var res_data_result_json = JSON.parse(res_data_json.result);
  return res_data_result_json.nli;
}

function getSttFromResult(res_data) {
  var res_data_json = JSON.parse(res_data);
  var res_data_result_json = JSON.parse(res_data_json.result);
  return res_data_result_json.asr.result;
}

//麥克風幀動畫 
function speaking() {
  var _this = this;
  //話筒幀動畫 
  var i = 1;
  this.timer = setInterval(function () {
    i++;
    i = i % 5;
    _this.setData({
      j: i
    })
  }, 200);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204

這部分主要實現錄音按鈕被按下和鬆開觸發話筒錄音及結束錄音,當按鈕被按下後,觸發呼叫話筒動畫特效(其實是四五個圖片輪流顯示的效果),同時呼叫wx.startRecord開始錄音。

當按鈕鬆開時停止錄音,然後將錄音臨時檔案往 https://api.happycxz.com/wxapp/silk2asr 上傳送,同時發olami上註冊申請的appKey和appSecret,以及使用者唯一識別號。

從語音識別介面返回的結果是原封不動的olami官方輸出結果,我們只需要取一下語音識別結果以及語義理解結果即可。 語義理解結果在原來 微信小程式——智慧小祕“遙知之”原始碼分享(語義理解基於olami)(注:這個是原來不支援語音識別的版本) 中已經有方法解析,為了程式碼複用性強些,把解析nli結果的方法簡單改了下,即適用新版語音識別的,也適用以前老版本的手動輸入的。

程式碼邏輯很簡單,看看就明白了,這裡不再多述。:)

asr.json原始碼:

{
  "window": {
    "enablePullDownRefresh": false
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

因為老版專案中我開啟了下拉重新整理,新介面上不需要了,所以在asr頁面的.json這裡特意關閉了此功能。

asr.wxml原始碼:

<view class="container">
  <view class="page-section">
    <view class="text-box" scroll-y="true">      
      <text style="max-width:200px;overflow-y:auto;height:200px;" selectable="true">{{outputTxt}}</text>
    </view>
  </view>

  <view class="page-section">
    <text selectable="true" class="text-head">語義理解基於olami.ai,作者QQ:404499164</text>
  </view>

  <view class="little-gap-top button-selection2 button-show bottom-button">
    <button size="mini" type="default" open-type="contact">聯絡作者</button>
    <button size="mini" type="default" bindtap="turnToOld">切老版本</button>
    <button size="mini" type="default" open-type="share">幫忙分享</button>
  </view>

  <view class="page-section">
    <view class="doommview">
      <block wx:for="{{doommData}}" wx:key="id">
          <text wx:if="{{item.display}}" class="aon" style="animation: first {{item.time}}s linear infinite;top:{{item.top}}%;color:{{item.color}};">
              {{item.text}}
          </text>
      </block>
    </view>
  </view>

<view  wx:if="{{isSpeaking}}"  class="speak-style">
  <image class="sound-style" src="../../pics/voice_icon_speech_sound_1.png" ></image>
  <image wx:if="{{j==2}}" class="sound-style" src="../../pics/voice_icon_speech_sound_2.png" ></image>
  <image wx:if="{{j==3}}" class="sound-style" src="../../pics/voice_icon_speech_sound_3.png" ></image>
  <image wx:if="{{j==4}}" class="sound-style" src="../../pics/voice_icon_speech_sound_4.png" ></image>
  <image wx:if="{{j==5}}"class="sound-style" src="../../pics/voice_icon_speech_sound_5.png" ></image>
</view>

</view>


<view class="record-style">
  <button type="primary" class="btn-style" bindtouchstart="touchdown" bindtouchend="touchup">按下錄音,鬆開結束</button>
</view>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

佈局調了半天,還是沒有達到我想要的效果,前端佈局我沒系統學習過,基本就是湊湊拼拼,望有基本審美觀的各位看官理解……

asr.wxss原始碼:

/* pages/asr/asr.wxss */

page{
  background-color:beige;
  background-image: url(https://img-blog.csdn.net/20170720105808995?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFwcHljeHo=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast);
  background-size: cover;
}

.page-section{
  display: flex;
  flex-direction: column;
  margin-bottom: 10rpx;
}

.text-head{
  color: #ff0000;
  font-size: 28rpx;
  align-items: center;
  margin-top: 5rpx;
}

.button-selection {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.button-selection2 {
  justify-content: space-between;
  align-content: space-between;
  flex-shrink:1;
}

.little-gap-top {
  margin-top: 10rpx; 
}

.big-gap-top {
  margin-top: 100rpx; 
}

.button-show {
  display: flex;
  align-self: center;
  justify-content: center;
}

.bottom-button {
  justify-content: space-around;
  flex-shrink:0;
}

.text-box{
  margin-bottom: 0rpx;
  margin-left: 50rpx;
  margin-right: 50rpx;
  padding: 40rpx 0;
  display: flex;
  min-height: 650rpx;
  max-width: 600rpx;
  width:600rpx;
  background-color: #ffffff;
  justify-content: center;
  align-items: center;
  text-align: left;
  font-size: 30rpx;
  color: #353535;
  line-height: 2em;
  word-wrap: break-word;
  border: 1px solid cornflowerblue;
}


/* 錄音 */
.speak-style{ 
    position: relative; 
    height: 240rpx; 
    width: 240rpx; 
    border-radius: 20rpx; 
    margin: 0 auto; 
    background: #26A5FF; 
} 
.record-style{ 
    position: fixed; 
    bottom: 0; 
    left: 0; 
    height: 120rpx; 
    width: 100%; 
} 
.btn-style{ 
  margin-left: 30rpx; 
  margin-right: 30rpx; 
} 

.sound-style{ 
  position: absolute; 
  width: 74rpx; 
  height:150rpx; 
  margin-top: 45rpx; 
  margin-left: 83rpx; 
} 


/* 彈幕 */
.button{
  position: absolute;
  bottom: 0;
  width: 100%;
}
.aon{
position: absolute;
white-space:nowrap;
animation-timing-function: linear;
animation-fill-mode: none;
}
.doommview{
  z-index: 3;
  height: 80%;
  width: 100%;
/*  position: absolute;  */
}

@keyframes first{
  from{left: 100%; }
  to{left: -100%;}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

彈幕部分原先是硬程式碼實現的,後來在小程式聯盟裡請教後才得知,css裡有可以實現動畫特效的功能,就順便修改了一下。還是有點顯示方面BUG的,不折騰了。

其它程式碼還是參照我原來的那個文章裡介紹的吧:微信小程式——智慧小祕“遙知之”原始碼分享(語義理解基於olami)(注:這個是原來不支援語音識別的版本) ,基本變動比較少。

想要全域性直觀的看完整程式碼,歡迎訪問該專案對應碼雲連結:http://git.oschina.net/happycxz/nlp_olami_yaozhizhi_wechat_littleapp_demo

>>>>>>>>>>>>>>>>>>>>>>>> 歡迎轉載 <<<<<<<<<<<<<<<<<<<<<<<<

本文原地址:http://blog.csdn.net/happycxz/article/details/78024986

“遙知之”微信小程式完整原始碼下載:

碼雲:http://git.oschina.net/happycxz/nlp_olami_yaozhizhi_wechat_littleapp_demo

github: https://github.com/happycxz/nlp_olami_yaozhizhi_wechat_littleapp_demo

寫在最後

這次小程式的版本更新,還是上一次的延續,上次老版本未能支援上語音識別,用起來很不方便,網上也找不到相應的免費的介面,於是索性湊了點時間專門自己搭個HTTPS服務出來,方便同樣想在微信小程式上DEBUG語音識別功能的夥伴們和興趣開發者們除錯和做些小玩意,好在,總算是一路走過來了,特別感謝kn007大神提供的silk decoder原始碼以及ffmpeg轉碼指令碼,關於此議題(解碼轉換QQ微信的SILK v3編碼音訊為MP3或其他格式)在他本人的部落格中火熱地討論了一年多了,感興趣的也可以去膜拜一下這位大神。

版權宣告:博主原創文章,歡迎大家轉載!

相關文章