FaceAPIを使った年齢判定LINE BotをNode.jsで作ってみた


最新のコードと手順書は、Githubにあります

[linefacebot](https://github.com/s0raySoA/linefacebot)

## Messege API

[LINE Developers](https://developers.line.me/ja/)

アクセストークンをコピーしておく

## Face API

1分あたり20トランザクションまで、できるため、とりあえずはFree価格で十分足りる

[価格 - Face API | Microsoft Azure](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/face-api/)

KEYをコピーしておく

## Function App

1. 新規作成

### HttpTriggerの関数を作成

LINEから来たメッセージを処理する関数

1. [新しい関数]を選択
2. `HttpTrigger`のテンプレートを選択
3. 言語:`JavaScript`、承認レベルは`Function`の関数を作成
4. [開発]タブを選択し、LINEからHTTP送信されたBodyを`Azure Queue Storage`に設定する処理を追加(index.js)
5. `Azure Queue Storage`へのバインド設定を追加する(function.js)

関数名: HttpTriggerJS1
Azure Queue Storage: outputQueueItem
キュー名: js-queue-items
ストレージアカウント接続: AzureWebJobsDashboard

```index.js
module.exports = function (context, req) {
  context.bindings.outputQueueItem = req.body;
  res = { body : "" };
  context.done();
};
```

```function.js
{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    },
    {
      "type": "queue",
      "name": "outputQueueItem",
      "queueName": "js-queue-items",
      "connection": "AzureWebJobsDashboard",
      "direction": "out"
    }
  ],
  "disabled": false
}
```

### QueueTriggerの関数を作成

キューに設定された内容を処理する関数を作成

1. [新しい関数]を選択
2. `QueueTrigger`のテンプレートを選択
3. 言語:`JavaScript`、関数HttpTriggerJS1と一致する`キュー名`と`ストレージアカウント接続`を入力し関数を作成
4. [開発]タブを選択し、`Azure Queue Storage`に設定された内容を取得し、画像ならFaceAPIに渡し、年齢をLINEに送信する処理を追加(index.js)


関数名: QueueTriggerJS1
キュー名: js-queue-items (関数HttpTriggerJS1と一致)
ストレージアカウント接続: AzureWebJobsDashboard (関数HttpTriggerJS1と一致)



```index.js
const https = require('https');
const url = require('url');

const FACE_API = 'https://australiaeast.api.cognitive.microsoft.com/face/v1.0/detect?returnFaceId=true&returnFaceLandmarks=false&returnFaceAttributes=age,gender,smile';

/**
 * JavaScript queue trigger function processed work item
 * @param {*} context
 * @param {*} myQueueItem
 */
module.exports = function(context, myQueueItem) {
  myQueueItem.events.forEach((event) => postMessage(context, event));
  context.done();
};

/**
 * Determining the message type
 * @param {*} context
 * @param {*} event
 */
function postMessage(context, event) {
  var messageType = event.message.type;
  if (messageType === 'text') {
    postLineMessage(context, event, '画像をおくってください');
    // postCognitiveUrl(context, event);
  } else if (messageType === 'image') {
    getImageData(context, event)
    .then((postData) =>{
        postCognitiveImage(context, event, postData);
    })
    .catch((err) => {
      context.log(err);
      postCognitiveImage(context, event, err);
    });
  } else if (messageType === 'sticker') {
    postLineMessage(context, event, '画像をおくってください');
  } else {
    postLineMessage(context, event, '画像をおくってください');
  }
}

/**
 *  Send image data to Face API
 * @param {*} context
 * @param {*} event
 * @param {*} postData
 */
function postCognitiveImage(context, event, postData) {
  var parseUrl = url.parse(FACE_API);
  var postOptions = {
      host: parseUrl.host,
      path: parseUrl.path,
      method: 'POST',
      headers: {
          'Content-Type': 'application/octet-stream',
          'Content-Length': postData.length,
          'Ocp-Apim-Subscription-Key': process.env.COGNITIVE_KEY,
      },
  };

  var bodyString = null;

  var req = https.request(postOptions, function(res) {
     context.log('STATUS: ' + res.statusCode);
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      bodyString = chunk;
    }).on('end', function() {
      if (res.statusCode !== 200 || bodyString === '[]') {
        postLineMessage(context, event, '顔が認識できませんでした');
      } else {
        var result = JSON.parse(bodyString);
        var age = result[0].faceAttributes.age;
        postLineMessage(context, event, age + '歳');
      }
    });
  });
  req.write(postData);
  req.end();
}

/**
 * Send message to LINE
 * @param {*} context
 * @param {*} event
 * @param {*} msg
 */
function postLineMessage(context, event, msg) {
  var jObj = {};
  jObj.type = 'text';
  jObj.id = event.message.id;
  jObj.text = msg;

  var postData = JSON.stringify({
    'replyToken': event.replyToken,
    'messages': [jObj],
  });

  var parseUrl = url.parse('https://api.line.me/v2/bot/message/reply');
  var postOptions = {
    host: parseUrl.host,
    path: parseUrl.path,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer {' + process.env.LINE_CHANNEL_ACCESS_TOKEN + '}',
      'Content-Length': Buffer.byteLength(postData),
    },
  };

  var req = https.request(postOptions);
  req.write(postData);
  req.end();
}

/**
 * Retrieve image data from LINE
 * @param {*} context
 * @param {*} event
 */
function getImageData(context, event) {
    return new Promise((resolve, reject) => {
        var messageId = event.message.id;
        var parseUrl = url.parse('https://api.line.me/v2/bot/message/' + messageId + '/content');
        var postOptions = {
        host: parseUrl.host,
        path: parseUrl.path,
        method: 'GET',
            headers: {
                'Content-type': 'application/json',
                'Authorization': 'Bearer {' + process.env.LINE_CHANNEL_ACCESS_TOKEN + '}',
            },
        };
        var req = https.request(postOptions, function(res) {
            var data = [];
            res.on('data', function(chunk) {
                data.push(new Buffer(chunk));
            }).on('error', function(err) {
                context.log(err);
                postLineMessage(context, event, err);
                reject(err);
            }).on('end', function() {
                var postData = Buffer.concat(data);
                resolve(postData);
            });
            });
        req.end();
    });
}
```

```function.js
{
  "bindings": [
    {
      "name": "myQueueItem",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "js-queue-items",
      "connection": "AzureWebJobsDashboard"
    }
  ],
  "disabled": false
}
```

※ コードはエラー処理も足りないと思うので自己責任で、コピーまたは、編集してください

### それぞれのアプリケーションを接続

1. `Function API`のアプリケーション設定を開く
2. アプリ設定に`Face API`のKEY、とLINEの`Messaging API`のKEYを環境変数に設定
3. HttpTriggerの関数のURL(エンドポイント)をコピーする
4. LINEの`Messaging API`のWebhookに`Function App`のエンドポイントを指定する

| 環境変数 | KEY |
| :---: | :---: |
| COGNITIVE_KEY | FaceAPIのKEY |
| LINE_CHANNEL_ACCESS_TOKE | Messaging APIのアクセストークン |


## 画像をLINEBoTに送信してみる
年齢が返ってくれば成功
以下は、フリー画像の美少女さんを使わせて頂いています

![faceBot-test.jpg](https://qiita-image-store.s3.amazonaws.com/0/248123/662acfe0-cc33-3c08-9200-6daad2e3b509.jpeg)