2017/6/12

開始寫第一個 Alexa Skill – Hello World!

目前我們或許還有機會可以選擇讓自己不被Amazon賺到錢, 但是在未來我們應該至少要學會/熟悉在這「Alexa Skill生態系」中獲利的方法與謀生的本領. Amazon於去年也釋出了Alexa Skill開發平台與語音模擬器, 因此使用者不需要擁有Echo也能開發「Alexa Skill Compliant」的智慧家電產品與應用. (今天查了一下官網, Echo Dot不到40美金就能入手了)



Alexa Ecosystem
誠如筆者於前一篇文章「Echo Dot 開箱--我的Alexa會說台灣國語」中所說的: Amazon自己擁有非常龐大的數據資料庫, 除了本身的通路, 它也有自己鏈結的音樂/圖書/App商城、配合的交通/旅行/購物服務與許多其它日常生活供應的平台商, 它幾乎可以達成客戶不須經過Google的廣告模式就能在它一站式服務平台內完成消費.

Alexa語音助理則進一步開啟了許多新的商業模式, 例如「Alexa Skill Store」與「Alexa Ecosystem, 即未來的家電商品若能支持「Alexa Skill Compliant」就有機會賺取打著「AI」旗幟的附加價值(儘管它本質上不算有AI). 其它各種衍生的服務, 如訂票(交通/住宿)、訂餐(Pizza)、叫車(Uber) Maker最愛的IoT互連與Smart Home控制等商機絡繹不絕.



Alexa運作的概念
一般初學任何自己陌生的程式語言都要來個「Hello World!, 然而Alexa Skill算是進入門檻較煩雜的. 並不是困難, 而是初學者必須先了解整個Alexa Skill背後運作的機制與框架(framework)才能比較容易入門, 因此筆者嘗試用下面這張簡圖說明Alexa語音助理背後資料的路徑與Skill Kit之間是如何運作的.




假設以使用者發出「Alexa, ask myApp, hello!」為例, 其語音串流語與資料路徑摘要如下:
1.     Echo本身會不停的偵測「Alexa」這個關鍵字pattern, 以叫醒 Echo並將Alexa後面的voice stream送到雲端的AVS (Alexa Voice Service) Server
2.     AVS將語音串(voice stream)成功辨識為文字並切割成「要被激活的App (Invocation),本例為「myApp. 以及同樣已被辨識為文字的剩餘命令句/語彙並稱之為「意圖(Intent), 本例為「hello. (: 不好意思用了許多「晶晶體」描述, 以上只是筆者主觀認為比較貼切的翻譯)
3.     AVS根據invocation name找到對應的客製化技能(Custom Skill), 並將「Intent」以JSON轉送到雲端的ASK (Alexa Skill Kit) Server
4.     ASK Server將「意圖(Intent)」語彙根據客製化技能(Custom Skill)內所列舉的一些範例語彙(sample utterances)做樣本比對(pattern matching), 若符合則觸發該「Intent
5.     ASK Server將「Intent」觸發/遞送(JSON)到以AWS (Amazon Web Service)/Lambda為基礎開發環境的客製化軟體(Skill App)
6.     開發者必須在客製化軟體(Skill App)自行撰寫該「Intent」的處理處理函式(handler)
7.     客製化軟體(Skill App)將處理結果、語音與提示文字等傳回AVS Server, 其中語音的部份會傳回Echo播出, 而文字提示則透過Alexa App於手機螢幕顯示

AWS/Lambda也支援第三方的IoT資料庫(或事件觸發資料庫)存取, 如圖中下方的資料途徑(為選項), 因此可以容易客製化/實作Smart Home的衍伸應用, 例如Alexa Skill Compliant家電開發. 其中底層資料存取/傳輸支援符合REST風格的Web API, 例如於之前「ESP8266存取IoT數據資料庫」文中提到的HTTPS/GET的資料請求方法.



程式撰寫概念
請參照最前面的Alexa語音服務的運作流程圖, 就初學者而言, Alexa Skill程式的撰寫與其開發環境/架構目前分成兩個區塊協同運作, 也就是由兩個不同的網頁/服務所構成, 因此您必須先申請這兩個平台的開發者帳號

右半部是很早期便已經存在並提供服務的Amazon Web Service (AWS) 平台, 它讓使用者無須申請固定IP與架設任何基礎建設就能建構網頁伺服器, 其中我們將使用到的Lambda平台允許使用者以JavascriptPython撰寫客製化的網際網路存取服務. 我們將在此為Alexa實作底層當所定義的「意圖(Intent)」被ASK觸發時的處理函式.



左半部則是新的Alexa Skill Kit (ASK) 開發者平台, 我們將在此為Alexa定義新的技能. 如上圖所示(再搭配最前面第一張Alexa運作流程圖), 當使用者「調用(Invoke)」自己寫的程式「myApp, 緊接著使用者的「命令語彙(utterance)」符合所預先例舉的樣本而觸發使用者自訂的「意圖(Intent), 本例為「HelloIntent, 並以JSON方式要求AWS/Lambda提供底層的服務(Intent Handler).



申請Alexa Skill Kit開發者帳號:
這個帳號目前免費使用, 如下圖登入之後選擇Alexa>Alexa Skill Kit即可為Alexa添增新增新的客制化技能(Custom Skill). 包括為這項新技能定義各種「意圖(Intent), 你可以透過JSON格式定義多項意圖(Intent Schema), 並預先設想/列舉各種可能透過自然語言觸發該意圖的各種「語彙表達方式(Sample Utterances), 最後再為觸發/激活該技能的App命名(Invocation). 




申請AWS/Lambda開發者帳號
這個帳號須綁信用卡, 但允許一年免費試用. 其實嚴格來說, 應該是一年過後, 筆者可能得付錢給Amazon再免費奉送Alexa新的技能. 這跟台灣許多電腦週邊供應商免費幫微軟(Microsoft)開發驅動程式卻得付錢給微軟拿才能到WHQL是同樣道理. (很可惜, 台灣許多公司賺到錢並沒有往建構類似共創經濟的生態系或平台等方向發展)

如下圖登入之後選擇我的帳戶>AWS管理主控台>Compute>Lambda即可新增新的App(所觸發Intent的處理函式).






Alexa增加說「Hello World!」的Skill
如我們前一個案例「我的Alexa會說中文」中所述, 事實上Alexa比較像是一連串「聲音語彙(utternaces)」的模式匹配(pattern matching)、「意圖(intent)」的查表與對應到「意圖(intent)」處理函式服務的軟體, 它本身不算有AI(至少筆者寫這篇文章時還沒有). 你給了不在標準開發者列舉範圍內的指令, 她無法自行處理或學習, 此時你必須去找一個符合需求的Skill來安裝, 就好像真的讓她學會一樣. 倘若完全找不到符合的技巧(Skill), 我們就必須自己寫一個. 本案例就是讓Alexa, 好像學會/聽得懂使用者命令句「hello」並產生互動說出: Hello World!.

Step1: Skill Information
ASK網頁新增一個叫做「my App」的Skill, Skill名稱目前對初學者而言不重要, 取一個好記的名稱即可. 只有將來我們功力進步想發佈到 public domain時可能會影響搜尋的順利, 因為你將發現Alexa Skill Store也是有許多不是我們要的App(取菜市場名稱可能很難找到我們自己開發的).

Invocation Name比較重要, 這是讓Alexa知道要調用(Invoke)哪個App (Skill)的名稱(未來我們可能開發或安裝很多個), 填完之後按儲存直接進下一步.



Step2: Interaction Mode

ASK/Intent Schema
如題, 我們想為Alexa添增/學會接受語音指令「hello」的新技能. 請搭配參閱前面程式撰寫的概念圖, 我們稱此命令句或語彙為「意圖(Intent). 意圖模式(Intent Schema)是以JSON的格式來定義. 如下, 我們為自己的Skill添增一個稱為「HelloIntent」的意圖.

{
  "intents": [
    {
      "intent": "HelloIntent"
    }
  ]
}



ASK/Sample utterances
為了讓Alexa能成功辨識使用者語音指令「hello」並觸發我們所定義的「HelloIntent, 因此我們須先預想幾種可能的使用者招喚方式, 以成功觸發該「意圖(Intent)」例如:

HelloIntent hello
HelloIntent say hello
HelloIntent please say hello
HelloIntent could you please say hello
HelloIntent hi



Step3: 撰寫Lambda程式
這個步驟須將焦點暫時從ASK網頁移到AWS/Lambda網頁, 並為我們設定的「HelloIntent」意圖實作底層的處理函式. 選擇Create建立新的Lambda Function, 此時系統會提供幾種現成的程式藍圖/範本當開發者的起點, 本例我們選擇空白的藍圖.




Lambda本身是提供多種事件觸發/驅動的網路伺服器, 開發者須指定Lambda程式將透過Alexa Skill Kit來觸發/驅動, 如下圖.




緊接著系統要求開發者為App命名, 請填入自己日後好記/區別Function功能的名字即可. 開發語言(支援JavascriptPython)部份請選擇Node.js, 本例我們將使用Javascript實作. Code entry部份, 我們可以先暫時不理它跳過.

 

關於Lambda網際網路服務與資料存取安全性相關的部份, 系統要求開發者選擇其中一項角色(role), 我們直接選lambda_basic_execution即可, 若第一次使用選擇建立一個custom role即可出現在existing role選單.












之後按下一步即可選擇建立新的Function, 請檢查Triggers是否已綁定由Alexa Skill Kit觸發. 接下來, 我們直接將下面的Javascript取代之前系統幫我們產生的幾行程式碼. 下面的程式碼也可以保留著當我們下一個專案的樣板. 程式真正需要我們增加或修改的部份只有從Start your codeEnd your code註解之間的幾行, 其餘都不要動到它們.

其中onIntent是當使用者的語音指令(utterances)符合(match)我們所列舉的樣本並觸發相對應的意圖(Intent), ASKJSONIntent字串傳給Lambda Function. 因此我們必須在此判斷是甚麼Intent? 以及提供相對應的Intent Handler. 以本例我們要處理的是被語音「hello」觸發(pattern match)並轉成文字的HelloIntet, 其相對的handler我們寫在 helloIntent函式, 該函式將送出「Hello World」語音(ASK傳回Echo)與文字(透過綁定App的手機)回覆.

'use strict';

/// Start your code below ////////////////////////////////////////////////////////
// HelloIntent (defined in ASK/intent_schema) handler
function helloIntent(intent, session, callback) {
    var cardTitle = 'Hello';
    var speechOutput = "Hello Worold!";
    var repromptText = speechOutput;
    var shouldEndSession = true;
    callback({},
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// Called when the user specifies an intent for this skill.
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if ( intentName === 'HelloIntent') {
        helloIntent(intent, session, callback);
    } else {
        handleSessionEndRequest(callback);
    }
}

/// End your code above ////////////////////////////////////////////////////////

// ASK: Default Intent Handler
function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    const cardTitle = 'Welcome';
    const speechOutput = "Welcome to Alexa Skill Kit. It's your first Alexa Skill.";
    const repromptText = speechOutput;
    const shouldEndSession = false;

    callback({},
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
    // Setting this to true ends the session and exits the skill.
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}


// -------------- Helpers that build all of the responses ----------------------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}

// --------------- Events ------------------------------------------------------
// Called when the session starts.
function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId},
sessionId=${session.sessionId}`);
}

// Called when the user launches the skill without specifying what they want.
function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

// Called when the user ends the session.
// Is not called when the skill returns shouldEndSession=true.
function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId},
sessionId=${session.sessionId}`);
    // Add cleanup logic here
}

// ------------------------ Main handler ---------------------------------------
// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {
        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};



按儲存之後請將網頁編輯器最右上方的ARN碼複製下來, 它將用來綁定ASK的外部服務service endpoint, 它的格式為: arn:aws:lambda:us-east-1:10483464xxxx:function:myApp



Step4: Configuration
完成Lambda程式並取得ARN碼之後我們須回到ASK的頁面繼續完成configuration, 如下圖, service enpoint中勾選AWS Lambda並填入ARN code.



Step5: 測試
ASK提供了文字轉語音輸出的模擬器與Echo語音助理的模擬器, 因此即使我們手邊並沒有真正的Echo硬體設備也能模擬真實的結果. 從模擬器中可以清楚看見底層資料以JSON的遞送/交換過程, 當使用者說出「hello, ASK從列舉的utternaces中成功辨識/匹配的「HelloIntent」並以JSON交由Lambda處理, 最後我們寫的Intent Handler函式處理完之後再以JSON傳回ASK接手後續的Echo發聲與手機文字顯示等服務.












測試步驟:
筆者: Alexa, hello
筆者: Alexa, hi
(載入自己寫的App重測)
筆者: Alexa, ask myApp, hello
Alexa: Hello World!