Firebaseで作る中耳炎判定LINEボット

皆さん、医療は好きですか？
画像判定に興味ありますか？
今回は中耳炎判定LINEボットを作りかたを紹介します。
この演習ではFirebaseとJavaScriptを扱います。プログラミング入門者に向けてテンプレートを用意してありますので1時間程度作れると思います。一緒に作ってみましょう！

### 免責事項
今回作成するツールはスキル獲得を目的とした実験ツールであり、実際の診断支援に使用するものではなく、患者に使用させるものでもありません。医師による診断を介してから患者側に結果を返すべきですが、今回は演習用ということで手順を省略しています。
臨床で使用される際は自己責任でお願いします。

## 対象者

- 必須
    - コンピュータとブラウザの基本的な操作ができる人
    - GoogleアカウントとLINEアカウントを持っている人、つまりGmailとLINEを使っている人
- 推奨
    - ターミナルのコマンドと引数の概念が分かる人
    - ひとつ以上のプログラミング入門をこなした経験がある人（理解レベルは問わず）


## 事前準備

- PCのOSを最新版にアップデートしておきましょう
- VisualStudio Codeのインストール
    - https://azure.microsoft.com/ja-jp/products/visual-studio-code/
- NodeJSのインストール
    - https://nodejs.org/ja/
    - 「推奨版」をインストール

## 事前知識

今回の演習で使う用語なので覚えておきましょう

- 今回分類するものは、急性中耳炎、滲出性中耳炎、正常鼓膜の3種類
- 急性中耳炎は英語でaom(acute otitis media)
- 滲出性中耳炎は英語でOME(otitis media with effusion)

## 学習モデルの作成

こちらの記事に沿って学習モデルを作りましょう
https://qiita.com/kinoppix/items/cbcaf8937a24bfc7888f

モデルIDをメモしておいてください。
モデルIDは後半で使います。

## Firebaseのセットアップ

- Firebaseにログインします
    - こちらからログイン→ https://firebase.google.com/?hl=ja
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/84eb8e9b-84b3-46b0-190e-7e87d01ba538.png)

- 新規プロジェクトを作成していきます
 - 「コンソールへ移動」を選択
 - 「プロジェクトを追加」を選択 
 - プロジェクト名は英数字で入力。ハイフン使用可
 - プロジェクト名は他の人と名前が被らないようにしなければならない
     - 例：aombot-yamada-taro
 - Googleアナリティクスは無効にします
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/b2ae1a40-4a24-540e-e7d3-cdf71e64b99e.png)
　
- 左下のSpark 無料プランを Blaze 従量性プランにアップグレードする（変更後も無料の範囲内で利用できます）
- この画面は後で使うので、プロジェクトを作成できたらそのままにしておきましょう
 
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/2ffed32e-89b1-3edc-fdff-eecc0e420249.png)


# LINEボットを用意する

LINEボットを作成して、プログラムにつなぎこむための文字列（チャネルシークレットとチャネルアクセストークン）を取得します。手順は多いですが、ひとつひとつの操作は単純なので間違えないように進めていきましょう。

LINE公式アカウントには有料プランがあります。
学習用途であればフリープランで十分です。

- LINE公式アカウントにログインする
 - https://www.linebiz.com/jp/service/line-official-account/ 
- 新しいLINE公式アカウントを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/ea556b99-56ba-2add-523b-e38f1a191c17.png)
 - 会社/店舗情報を登録(必須部分は適当に何か入力してください)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/9060c261-9df4-6316-80b8-7fc43e33eda6.png)

- ボットと友だちになるための手順
 - LINE Official Account Managerへ移動します 
 - ホーム→左メニューの最下部「友だちを増やす」→友だち追加ガイドを開く
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/a44271d0-3e17-9488-97a6-383ceec85ba2.png)
 - スマホのLINEを立ち上げてQRコードを読み込む
 - 友だち追加する
- チャネルシークレットとチャネルアクセストークンを取得する手順
 - LINE Developersを開く
     - 「設定」（画面右上)→Messaging APIを開く     
     - 「Messaging APIを利用する」を開く
      ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/6694d91f-56cf-1491-b1d9-d0b2068e988d.png)

     - プロバイダー名を適当に入力(例：画像診断支援test）しプロバイダーを作成する
         - プロバイダー名はいつでも変更可能 
     - プライバシーポリシーと利用規定は空白のままでOK     
     - 「その他の設定はLINE Developersから行えます。」のリンクをクリック
     ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/f7715cac-605d-8ccf-89f1-f5b703481eb0.png)

     - 画面右上の「ログイン」ボタンを押してログイン
     ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/67c0353b-d9a0-d3ba-e9ea-abc514eebe38.png)

 - ログイン後、英語表示になっている場合日本語表示に切り替え
     - 画面右下の「English」をクリックして「日本語」に変更

   - 左の欄にある、作成したプロバイダー「画像診断支援test」を選択
     ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/bd76952b-7495-c166-7a1e-67fa3c0d6de0.png)　　　
   - 作成したチャネル「aomtest」を開く
   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/94b37f86-3487-8be3-8061-39ea7b50e9f5.png)

    - チャネルシークレットを取得していきます
        - 「チャネル基本設定」を開く
        - 下にスクロールして「チャネルシークレット」を見つける
        - 英数字の羅列の右端にあるコピー画像をクリック
        - 「コピーしました」と表示されれば成功
        - メモ帳に貼り付けをして退避させておきましょう
    - チャネルアクセストークンを取得していきます
        - 「Messaging API設定」を開く
        - 下にスクロールして「チャネルアクセストークン（長期）」を見つける
        - 英数字の羅列の右端にあるコピー画像をクリック
        - 「コピーしました」と表示されれば成功
        - メモ帳に貼り付けをして退避させておきましょう
- 挫折しないためのポイント
    - チャネルシークレットとチャネルアクセストークンを混同しないように注意しましょう
    - チャネルシークレットは短めの文字、チャネルアクセストークンは長い文字と覚えておきましょう

LINE Developersは開きっぱなしにしておきましょう。最後のステップで使います。


# VisualStudio Codeの準備

VisualStudio Code（以下VS Code）を使ってプログラムを準備します。

- VisualStudio Codeを最初に起動したときにやること
 - いきなり英語表示で戸惑うと思いますので日本語化しましょう。
 - 初回起動すると、右下に日本語パックのインストールのおすすめが表示されるのでインストールします
 - VS Codeを再起動する
- 空フォルダを作る。名前は「aombot」
- VS Codeの「フォルダーを開く」をクリックして、先程の空フォルダの場所を開く
    - 「フォルダーの選択」ボタンを押してフォルダを開いた状態にする
    - ファイルを選択するのは誤り。やってしまった場合はファイル→新しいウィンドウをクリックしてやり直す
- ターミナル→新しいターミナルをクリック、下側にターミナルが表示される

# プログラムを流し込む

Firebaseプロジェクトを作ります。ここからソースコードを扱います。
Visual Studio Codeのターミナルを使ってコマンド入力していきます。

【重要】カレントディレクトリ間違えると100%失敗しますので、カレントディレクトリを間違えないように指差し確認しながら進めましょう。

## カレントディレクトリとは

- カレントディレクトリとは、「現在開いているフォルダ」のターミナル版です。
- Macの場合、`pwd`と入力してエンターを押すとカレントディレクトリが表示されます。
- カレントディレクトリがaombotの場合、Windowsでは`aombot> `と表示されます。Macの場合は`aombot$ `と表示されます。
- この解説ではWindowsの表示に統一しています。Mac利用者は適宜読み替えてください。

- Firebaseコマンドをインストールする手順
    - カレントディレクトリはaombot、入力してもらいたいコマンドは`npm install -g firebase-tools`です
    - Windowsの場合：`aombot> npm install -g firebase-tools`
        - PowerShellのスクリプト実行をできる設定にする
        - PowerShellを「管理者として実行」
        -![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/4f276066-e68b-9e32-1c32-f9f03a2bbf31.png)
        - `PowerShell Set-ExecutionPolicy RemoteSigned`
        - https://qiita.com/Targityen/items/3d2e0b5b0b7b04963750
    - Macの場合：`aombot$ sudo npm install -g firebase-tools`
        - Macの場合は管理者権限でインストールするのでsudoが必要
        - 直後に求められるパスワードは、Macのログイン時のパスワードを入力します
    - ターミナルで`firebase`コマンドを実行して、画面いっぱいの英文メッセージが出ればインストール成功
        - 【Windows】「用語 'npm' は、コマンドレット、関数、スクリプトファイル、または操作可能なプログラムの名前として認識されません。」のエラーメッセージが表示される
            - Node.jsのインストールに失敗している。再起動してみよう。
        - 【Windows】「このシステムではスクリプトの実行が無効になっているため」のエラーメッセージが表示される
            - 上記のPowerShell Set-ExecutionPolicy RemoteSignedを実行すること
        - 【Mac】`sudo npm install -g firebase-tools`を実行するとパスワードを聞かれる
            - Macの起動時のパスワードを入力してください
        - 【Mac】「command not found: npm」と表示される
            - Node.jsのインストールに失敗しています。ターミナルを再度開き直してみてください。
        - 【Mac】「command not found: firebase」と表示される
            - firebaseのインストールに失敗している。sudoつけて実行しましたか？
- Firebaseプロジェクトとつなぎこみする手順
    - `aombot> firebase login`
        - aombotディレクトリ上で「firebase(半角スペース)login」と入力してEnterを押す、という意味
    - `aombot> firebase init`
        - aombotディレクトリ上で「firebase(半角スペース)init」と入力してEnterを押す、という意味
    - 既存のプロジェクトを選択
    - Functionsのみを選択
        - 上下キーで移動、スペースキーで選択、Enterキーで決定
    - TypeScriptかJavaScriptかを聞かれるので「JavaScript」を選択
    - Lintを使用するか聞かれるので「No」を選択
    - 途中で間違えたらCtrl+Cを押して中断できます。もし完了後に間違いに気づいたらフォルダを空にして、firebase initからやり直してください。
- npmパッケージのインストール
    - functionsフォルダに移動
        - `aombot$ cd functions`
        - ターミナルの表示が`functions>`(Windows)または`functions$`(Mac)になっていることを確認
    - `functions> npm install @line/bot-sdk`
    - `functions> npm install @sashido/teachablemachine-node`
    - `functions> npm install firebase-admin`
    - `functions> npm install firebase-functions`
- ソースコードを編集をしていきます
    - VS Codeの左メニューの「エクスプローラー」からfunctionsフォルダのindex.jsファイルをダブルクリックして開く
    - この記事の最下部にあるソースコードをindex.jsに貼り付け
    - ファイルを保存（重要）
    - 4行目と5行目にメモ帳に退避させておいたチャネルアクセストークンとチャネルシークレットを貼り付け
        - シングルクォーテーション「'」で囲まれた状態にしましょう
        - 前後にスペースが入らないように気をつけましょう
        - 例：`channelSecret: '1aa123456789223456789',`
    - 9行目のmodelUrlの`******`には、冒頭のモデルIDを入力してください

# Firebaseにデプロイ

プログラムをFirebaseにデプロイ（展開）していきます。

- `functions> firebase deploy`（aombotディレクトリでもOK）
    - SyntaxError:と表示された場合、ソースコードの記述が誤っています。ソースコードの編集が解説どおりにできているか指差し確認しましょう。
- 5分ほど待ってDeploy complete!と表示されたらデプロイ成功


# LINE Botの設定

！！注意！！
ここから先の操作をするには、ここでデプロイに成功している必要があります。デプロイに成功していないとこの先の手順が全て失敗しますのでご注意ください。

- Firebaseの管理画面を表示
- 該当プロジェクトを開く
- 左メニューの構築→Functionsをクリック
- linebotのURLをコピーしていきます
    - 例えば以下のようなURL。`aom-yamada-taro`の箇所は人によって異なるので注意。
    - https://us-central1-aom-yamada-taro.cloudfunctions.net/linebot
- LINE DevelopersでWebhookの設定をしていきます
    - LINE Developersを開く
    - Messaging API設定を開く
    - Webhook設定までスクロールする
    - Webhook URLに先程のURLを貼り付ける
    - 「検証」を押す
        - 「成功」と表示されれば成功
        - エラーが表示された場合は再度「検証」を押してみる
        - それでも成功と表示されない場合以下を再確認しましょう
            - URLは正しいですか？
            - チャネルシークレットは正しいですか？
            - チャネルアクセストークンは正しいですか？
    - 「Webhookの利用」をオンにする

# 動作確認
- スマホのLINEから中耳炎画像を送信します
- 返信が来れば成功です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/5783e1bf-740d-62ae-5705-31015073fd2d.png)

# 応用編：LINE Botの見た目を良くする
- LINEの画像を設定
    - https://www.irasutoya.com/2016/05/blog-post_619.html
- 意図しない返信を抑制する
    - 設定→応答設定に移動
        - あいさつメッセージ：オフ（お好みで）
        - 応答メッセージ：オフ（お好みで）
    - 本来はあいさつメッセージを設定してあげると親切
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453374/6dc97a53-9960-a61b-5c46-c42bd38ed926.png)


# ソースコード
```javascript:index.js
'use strict';
const line = require('@line/bot-sdk');
const config = {
    channelSecret: '', // チャネルシークレットを貼り付け
    channelAccessToken: '' // チャネルアクセストークンを貼り付け
};
const client = new line.Client(config);

const functions = require("firebase-functions");
const TeachableMachine = require("@sashido/teachablemachine-node");
const model = new TeachableMachine({
    modelUrl: "https://teachablemachine.withgoogle.com/models/********/" // TeachableMachineのモデルのURLを貼り付け
});

exports.linebot = functions.https.onRequest(async (request, response) => {
    Promise
        .all(request.body.events.map(handleEvent))
        .then((result) => response.json(result));
});
async function handleEvent(event) {
    //画像以外は受け付けない
    if (event.type !== 'message' || event.message.type !== 'image') {
        return;
    }

    //画像のデータを取得
    const stream = await client.getMessageContent(event.message.id);
    const content = [];
    for await (const chunk of stream) {
        content.push(Buffer.from(chunk));
    }
    const dataScheme = 'data:image/jpeg;base64,' + Buffer.concat(content).toString('base64');
    // functions.logger.info(dataScheme);

    //分類器にかける
    const predictions = await model.classify({
        imageUrl: dataScheme,
    });
    
    functions.logger.info(predictions);
    return client.replyMessage(event.replyToken, {
        type: 'text',
        text: "結果：" + Math.floor(predictions[0].score*100)+"%の確度で"+predictions[0].class+"だと思います。"
    });
}
```

## お片付け

FirebaseのBlazeプランは月間のアクセス数が増えると料金が発生します。長らく使わないようでしたら無料プランに戻すことも検討してください。ただし無料プランにすると`firebase deploy`ができなくなるので注意してください。
今後の開発のために残しておきたいひとはBlazeプランのままでも構いません。料金発生したとしても個人開発レベルであれば月額2円程度(2,000円ではなく「2円」)を見込んでおけば十分かと思います。
[firebaseの料金体系はこちら](https://firebase.google.com/pricing?hl=ja)

# さいごに

ハンズオンお疲れさまでした！最後までできた人も、途中までしかできなかった人も、デジタルヘルスの距離感がつかめたかと思います。デジタルヘルス学会ではデジタルヘルスプロダクトを作る皆さんを応援しています。

この記事は、第5回デジタルヘルス学会のハンズオン向けに作成されました。