部落格 > BLOG

手把手教你用 OLAMI + Facebook Messenger 製作語音聊天機器人

olami     2017-10-02 15:11

OLAMI 團隊很榮幸的受邀參與 Facebook 官方在地開發者社群 - Facebook Developer Circle: Taipei 與 Chatbot Developers Taiwan 社團共同舉辦的第四場活動『 Facebook Developer Circle: Taipei - Meetup #4 』,並在活動中介紹 OLAMI 平台,以及如何用 OLAMI APIs 打造 Facebook Messenger Bot。

Keynotes:


• OLAMI 平台簡介

• 案例分享


  • OLAMI x Chatbot ( Device / Robot ) 
  • OLAMI x Chatbot ( AR / VR )
  • OLAMI x Chatbot x Facebook Messenger 




案例分享&教學:

手把手教你用 OLAMI + Facebook Messenger 製作語音聊天機器人


講者:Roger ( OLAMI 軟體工程師 )


哈囉大家好!以下教大家怎麼快速建立Facebook messenger chatbot 吧~~

先附上我參考的資料

1. OLAMI 雲端語音辨識 API:https://tw.olami.ai/wiki/?mp=api_asr&content=api_asr1.html
2. OLAMI 自然語言語意理解 API :https://tw.olami.ai/wiki/?mp=api_nlu&content=api_nlu1.html
3. Facebook for Developers : https://developers.facebook.com/
4. Facebook Messenger 平台開發者說明文件:https://developers.facebook.com/docs/messenger-platform

讓我們開始吧!



第一章:準備 Facebook 開發環境

Step 1. 註冊為 Facebook 開發者

登入你的 Facebook 帳號,並到 Facebook 開發者平台「https://developers.facebook.com/」將自己的帳號註冊為開發者,這樣才能進行後續的開發喔


Step 2. 建立「粉絲團」

你必須要有管理粉絲團的權限(管理員),如下圖,如果沒有,就必須要自己建立一個粉絲專頁,可以點擊以下連結前往建立

建立Facebook粉絲專頁:https://www.facebook.com/pages/create/?ref_type=logout_gear


Step 3. 建立應用程式

在 Facebook Developers 平台上方選單點選「我的應用程式」,點選應用程式

接著輸入應用程式名稱,這樣就建立完成囉!!


Step 4. 產生權杖

點選左邊的「新增產品」,接著點選 Messenger 中的「設定」

接著在「權杖產生 (access token )」這邊,取得粉絲頁的權杖這樣才能開始使用 API

就會得到相對應的權杖,可以先複製起來,等等驗證Webhook會需要用到它


Step 5. 設定和啟動Webhook Server

下載 Facebook Dev 提供的原始碼,可以讓開發者快速的啟動 Server
原始碼下載位置:https://gomix.com/#!/project/messenger-bot

在.env檔案當中將你上一步驟,所得到的權杖輸入進去(PAGE_ACCESS_TOKEN=[你的權杖])

並且在「VERIFY_TOKEN」部份輸入你要驗證的token

接著,就可以將檔案上傳至你的伺服器當中,並且啟動它「node   bot.js」,成功啟動的話,瀏覽器應該會看到以下頁面

Step 6.  驗證Webhook

點選「設定Webhook」開始進行驗證的部份

接著輸入回呼網址(必須要為https://開頭的網址並後面帶/webhook的回呼網址,例如:https://example.com/webhook),和驗證權杖(是輸入在 Step5. 在 .env中輸入的VERIFY_TOKEN)

並且勾選「messages」和「messaging_postbacks」

然後按下驗證並儲存


如下圖所示,如果出現紅色叉叉的話,代表驗證失敗,請返回上一步,檢查伺服器的設定是否正確


如果驗證通過應該是會出現「完成」綠色勾勾,並且記得訂閱你要操作的粉絲頁喔

Step 7. 測試聊天機器人

接著就可以在自己的粉絲頁和機器人聊天了,它應該會回傳和你輸入一樣的字串



第二章:準備接入 OLAMI APIs

Step 1. 下載 OLAMI nodejs Github 專案

先至 Github 下載 OLAMi 提供的 nodejs API 和範例程式

原始碼下載位置:https://github.com/olami-developers/olami-api-quickstart-nodejs-samples

並將 NluApiSample.js 和 SpeechApiSample.js 這兩個檔案放到和 bot.js 同目錄。


Step 2. package.json 加上使用的套件

在這個專案的 package.json 的相依性 (dependencies) 加上以下資訊

//// file: package.js
.......略......
"dependencies": {
	"request": ">=2.75.0",
	"express": "*",
	"body-parser": "*",
	"bufferhelper": ">=0.2.1",
	"iconv-lite": ">=0.4.13",
	"md5": ">=2.2.1",
	"urlencode": ">=1.1.0",
	"delayed": ">=1.0.1",
	"child_process": ">=1.0.2"
}
.......略......


Step 3. 匯入API Library和建立物件

現在將 NLU 的 API 和 Speech 的 API 函式庫匯入進來,並建立物件

//// file: bott.js

.......略......

var olamiLocalizationUrl = 'https://tw.olami.ai/cloudservice/api';
var olamiAppKey = '*****your key********';
var olamiAppSecret = '*****your secret********';

var NLUApiSample = require('./NluApiSample.js');
var nluApi = new NLUApiSample();
nluApi.setLocalization(olamiLocalizationUrl);
nluApi.setAuthorization(olamiAppKey, olamiAppSecret);

var SpeechApiSample = require('./SpeechApiSample.js');
var speechApi = new SpeechApiSample();
speechApi.setLocalization(olamiLocalizationUrl);
speechApi.setAuthorization(olamiAppKey, olamiAppSecret);

.......略......


Step 4. 將使用者輸入文字接入 OLAMI 語意理解系統

在function receivedMessage當中取得使用者的文字輸入之後,透過 nluApi.getRecognitionResult() 將使用者輸入的文字以參數的方式傳入,並且將辨識結果以callback的方式回傳

//// file: bott.js

.......略......

// Incoming events handling
function receivedMessage(event) {
  var senderID = event.sender.id;
  var recipientID = event.recipient.id;
  var timeOfMessage = event.timestamp;
  var message = event.message;

  console.log("Received message for user %d and page %d at %d with message:",
  senderID, recipientID, timeOfMessage);
  console.log(JSON.stringify(message));

  var messageId = message.mid;
  var messageText = message.text;
  var messageAttachments = message.attachments;

  // 如果使用者輸入為文字的話
  if (messageText) {
    receivedUserTypeMessage(senderID, messageText);
  }
}

// 處理使用者輸入訊息
function receivedUserTypeMessage(senderID, userTypeMessage)  
        // If we receive a text message, check to see if it matches a keyword
	// and send back the template example. Otherwise, just echo the text we received.
	// 判斷使用者輸入的文字是否為關鍵字
	switch (userTypeMessage) {
		// case 'generic':
		//   sendGenericMessage(senderID);
		//   break;

		default:
                         // 將使用者輸入的資訊導入 OLAMI NLU API 當中
                        nluApi.getRecognitionResult("nli", userTypeMessage, function(resultArray) {
				var sendMessage = "";
				resultArray.forEach(function(result, index, arr) {
					sendMessage += result + "\n";
				});
				sendTextMessage(senderID, sendMessage);
			}, function(baikeArray) {   // 回傳值是百科內容,需要套用template去顯示 
                                var subtitle = "";
				baikeArray[1].forEach(function(item, index, arr) {
					subtitle += item +" : "+ baikeArray[2][index] + "\n";
				});
				sendTextMessage(senderID, baikeArray[0]);
				sendWikiTemplateMessage(
					senderID,
					baikeArray[2][0],
					subtitle,
					baikeArray
				);
			});
	}
}

.......略......


Step 5. 解析 OLAMI NLI 回傳

這邊我們建立了 DumpIDSData 的類別專門處理從 OLAMI IDS 回傳的 json 字串處理,包含食譜、百科、笑話和詩歌等回傳的資料

//// file: DumpIDSData.js

function DumpIDSData() {

}

DumpIDSData.prototype.getCookingResult = function(serverResponse) {
	var json = JSON.parse(serverResponse);
	var cooking = new Array(2);

	cooking[1] = json['data']['nli'][0]['data_obj'][0]['name'];
	cooking[2] = json['data']['nli'][0]['data_obj'][0]['content'];

	return cooking;
}

DumpIDSData.prototype.geWikiResult = function(serverResponse) {
	var json = JSON.parse(serverResponse);
	var baike = new Array(2);

	baike[1] = json['data']['nli'][0]['data_obj'][0]['field_name'];
	baike[2] = json['data']['nli'][0]['data_obj'][0]['field_value'];
	baike[3] = json['data']['nli'][0]['data_obj'][0]['description'];
	baike[4] = json['data']['nli'][0]['data_obj'][0]['photo_url'];

	return baike;
}

DumpIDSData.prototype.geJokeResult = function(serverResponse) {
	var json = JSON.parse(serverResponse);
	var joke = new Array(2);

	joke[1] = json['data']['nli'][0]['data_obj'][0]['content'];

	return joke;
}

DumpIDSData.prototype.gePoemResult = function(serverResponse) {
	var json = JSON.parse(serverResponse);
	var poem = new Array(2);

	poem[1] = json['data']['nli'][0]['data_obj'][0]['author'];
	poem[2] = json['data']['nli'][0]['data_obj'][0]['title'];
	poem[3] = json['data']['nli'][0]['data_obj'][0]['content'];

	return poem;
}

module.exports = DumpIDSData;

 

接著在 NluApiSample.js 中匯入剛剛建立的 DumpIDSData 類別並且建立物件,接著對程式進行改寫

////  file: NluApiSample.js

.......略......

var DumpIDSData = require('./DumpIDSData.js');
var dumpIDSData = new DumpIDSData();

.......略......

// 取得伺服器回傳的
NluApiSample.prototype.getMessageType = function(serverResponse) {
   var json = JSON.parse(serverResponse);
   var messageType = json['data']['nli'][0]['type'];
   return messageType;
}


// 取得伺服器回傳的訊息描述

NluApiSample.prototype.getMessageDescObj = function(serverResponse) {
   var json = JSON.parse(serverResponse);
   var messageDescObj = json['data']['nli'][0]['desc_obj']['result'];
   return messageDescObj;
}


NluApiSample.prototype.getRecognitionResult = function (
   apiName,
   input,
   resultCallback,
   structureCallback
) {

.......略......

        request.get({
		url: url,
	}, function(err, res, body) {
		if (err) {
			console.log(err);
		}
	}).on('response', function(response) {
		var bufferhelper = new BufferHelper();
		response.on('data', function(chunk) {
			bufferhelper.concat(chunk);
		});

		response.on('end', function() {
			var result = iconv.decode(bufferhelper.toBuffer(), 'UTF-8');

			// console.log("---------- Test NLU API, api = "+ apiName +" ----------");
			// console.log("Sending 'POST' request to URL : " + url);
			console.log("Result:\n" + result);
			// console.log("------------------------------------------");

			var messageType = nliApiSample.getMessageType(result);
			switch (messageType) {
				case 'cooking':
					var cookingReturn = dumpIDSData.getCookingResult(result);
					cookingReturn[0] = nliApiSample.getMessageDescObj(result);
					resultCallback(cookingReturn);
					break;

				case 'baike':
                                        // 如果是百科則需要回傳結構資料,以利於套用 template  
                                        var baikeReturn = dumpIDSData.geWikiResult(result);
					baikeReturn[0] = nliApiSample.getMessageDescObj(result);
					structureCallback(baikeReturn);
					break;

				case 'joke': var jokeReturn = dumpIDSData.geJokeResult(result);
					jokeReturn[0] = nliApiSample.getMessageDescObj(result);
					resultCallback(jokeReturn);
					break;

				case 'poem':
					var poemReturn = dumpIDSData.gePoemResult(result);
					poemReturn[0] = nliApiSample.getMessageDescObj(result);
					resultCallback(poemReturn);
					break;

				default:
					var resultArray = new Array(nliApiSample.getMessageDescObj(result))
					resultCallback(resultArray);
			}
		});
	});
	
   .......略......
   
}


Step 6. 開始測試

接下來就可以透過 Facebook Messenger 用文字和 OLAMI 聊天囉!並且伺服器可以成功接收到使用者回饋,和接收到 OLAMI 伺服器的回傳值



Step. 7 完成後原始碼

原始碼下載位置:https://github.com/Guantou-Li/olami-facebook-chatbot




第三章:使用 OLAMI 語音辨識 API 辨識錄音檔案

Step 1. 判斷使用者是否有上傳附件

首先先判斷使用者是否有上傳附件,並且將FB給我們的聲音檔案網址給解析出來

//// file: bot.js 
.....略.........
// Incoming events handling
function receivedMessage(event) {
  var senderID = event.sender.id;
  var recipientID = event.recipient.id;
  var timeOfMessage = event.timestamp;
  var message = event.message;

  console.log("Received message for user %d and page %d at %d with message:",
  senderID, recipientID, timeOfMessage);
  console.log(JSON.stringify(message));

  var messageId = message.mid;
  var messageText = message.text;
  var messageAttachments = message.attachments;

	// 如果使用者輸入為文字的話
	if (messageText) {
		receivedUserTypeMessage(senderID, messageText);
	// 如果使用者輸入的內容為附件的話
  } else if (messageAttachments) {
		var json = JSON.parse(JSON.stringify(messageAttachments));
		var attachmentType = json[0]['type'];
		switch (attachmentType) {
			case 'audio':
				// sendTextMessage(senderID, "請稍後,我們已經收到語音檔案附件,馬上為你進行辨識");
				receivedAudioAttachment(senderID, json[0]['payload']['url']);
				break;
			default:
				sendTextMessage(senderID, "收到附件");
		}
	}
}
.....略.........


Step 2. 取得聲音檔案進行聲音轉檔

請注意!轉檔環境需要用到 ffmepg 轉檔和 wget 套件,可透過 apt-get  install  ffmepg wget 安裝相關套件,透過 wget 下載聲音檔案到 upload_audio/ 資料夾當中,並且透過 ffmepg 轉檔將檔案 aac 轉換成採樣率為16K的 wav 檔案(ffmepg  -i  [檔名]  -ar  16000  [輸出檔名])

//// file: bot.js
.....略.........
// 處理使用者上傳的語音附近,並開始進行語音辨識
function receivedAudioAttachment(senderID, audioUrl) {
	console.log("開始下載語音檔案..."+ audioUrl);

	var spawn = require('child_process').spawn;

	//kick off process
	const wget = spawn('wget',
		['-O', 'upload_audio/'+ senderID +'.aac', audioUrl]);
	//spit stdout to screen
	wget.stdout.on('data', function (data) {   process.stdout.write(data.toString());  });
	//spit stderr to screen
	wget.stderr.on('data', function (data) {   process.stdout.write(data.toString());  });

	wget.on('close', function (code) {
		const ffmpeg = spawn('ffmpeg',
		['-i', 'upload_audio/'+ senderID +'.aac', '-ar', '16000', 'upload_audio/'+ senderID +'.wav', '-y']);
		//spit stdout to screen
		ffmpeg.stdout.on('data', function (data) {   process.stdout.write(data.toString());  });
		//spit stderr to screen
		ffmpeg.stderr.on('data', function (data) {   process.stdout.write(data.toString());  });
	});
}
.....略......... 


Step 3. 將語音檔案接入至 OLAMI 語音辨識系統

將已經轉檔之後的檔案,導入 speechApi當中,並且將 OLAMI 語音辨識後的回傳值,透過callback的方式回傳結果,在丟至 receivedUserTypeMessage function 處理要顯示的內容

//// file: bot.js
// 處理使用者上傳的語音附近,並開始進行語音辨識
function receivedAudioAttachment(senderID, audioUrl) {
	console.log("開始下載語音檔案..."+ audioUrl);
       .....略......... 

	wget.on('close', function (code) {
		.....略......... 

		ffmpeg.on('close', (code) => {
			console.log("開始進行語音辨識..."+ senderID +'.wav');
			sendSenderActions(senderID);
			speechApi.sendAudioFile(
				'asr',
				'nli',
				true,
				'upload_audio/'+ senderID+'.wav',
				0, function(sttText) {
					sendTextMessage(senderID, '辨識結果為:'+ sttText);
					receivedUserTypeMessage(senderID, sttText);
				}
			);
		});
	});
}


將 getRecognitionResult function 進行改寫,並且將 OLAMI 語音辨識的結果進行解析,將結果以callback方式回傳

////  file: SpeechApiSample.js
........    略  .......
SpeechApiSample.prototype.getRecognitionResult = function (apiName, seqValue, callbackFunction) {
	var _this = this;
	var url = this.getBaseQueryUrl(apiName, seqValue);
	url += '&stop=1';
	// Request speech recognition service by HTTP GET
	request.get({
		url: url,
		headers: {
			'Cookie': this.cookies
		}
	}, function(err, res, body) {
		if (err) {
			console.log(err);
		}
	}).on('response', function(response) {
		var bufferhelper = new BufferHelper();
		response.on('data', function(chunk) {
			bufferhelper.concat(chunk);
		});

		response.on('end', function() {
			var body = iconv.decode(bufferhelper.toBuffer(), 'UTF-8');
			var result = JSON.parse(body);
			var return_status = result['data']['asr']['final'];
			// Try to get recognition result if uploaded successfully.
			// We just check the state by a lazy way :P , you should do it by JSON.
			if (return_status !== true) {
				console.log("\n----- Get Recognition Result -----\n");
				// Well, check by lazy way...again :P , do it by JSON please.
				delayed.delay(function () {
					_this.getRecognitionResult('asr', 'nli,seg', function(asrResult) {
						callbackFunction(asrResult);
					});
				}, 500);
			} else {
				console.log("\n----- Get Recognition Result -----\n");
				console.log("Result:\n\n" + body);

				// 語音辨識後的結果
				var json = JSON.parse(body);
				var asrResult = json['data']['asr']['result'];
				console.log("ASR Result = "+ asrResult);
				callbackFunction(asrResult);
			}
		});
	});
}
........    略  .......


Step. 4 開始測試

Step. 5 完成後原始碼

原始碼下載位置:https://github.com/Guantou-Li/olami-facebook-chatbot


Copyright © 2017 威盛电子股份有限公司. All rights reserved   |   意见反馈