Djangoで猫を判別するLINE Chatbotを作ってみた

# はじめに
Djangoを使って、LINEアプリのチャットボットを作成した備忘録:writing_hand:

今回使ったAPIは
- LINE Messaging API
- Google Cloud Vision API
- DeepL API

# 開発環境
- macOS
- Python 3.10.10

# 構造
ざっくりとしたイメージ
1. チャットボットがメッセージを受信する
2. 画像メッセージであれば、猫かどうかを判別する
3. 判別結果に応じて、返信メッセージを作成
   猫でなければ、可能性のある候補を３つ提示
4. 返信メッセージを和訳して、日本語と英語のメッセージを返信する


![linebot_cat  (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/b9aca630-86ca-4423-8f08-d3c65906ccf8.png)


それでは、LINEチャットボットアカウントの作成と実際のpythonコードを見ていきます:robot:

# LINEチャットボットアカウントの作成と設定
### LINE Developersでボットアカウントを作成する
:arrow_right: [LINE Develpers](https://developers.line.biz/en/)にアクセスし、右上の`コンソールにログイン`から、ログイン・新規登録を行う
![Screenshot 2024-08-14 at 00.18.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/9d6108e8-10fb-cb51-92d0-1172b5232ac7.png)

:arrow_right: ログイン後、`コンソールトップ画面` > `プロバイダー`から`作成`を押して、ボットアカウントを作成する
![Screenshot 2024-08-14 at 00.28.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/ab04d49e-7eff-11f2-3804-1a30cab5b091.png)

:arrow_right: `Messaging API`を選択して、必須項目を入力する
![Screenshot 2024-08-14 at 00.40.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/38a49e4b-b84d-220c-05a9-b9847389547e.png)

ここでボットアカウント作成はひとまず完了:v:

# Djangoの実装
プロジェクト名: linebot
アプリ名: linebot_cat

以下はディレクトリツリーです
```
linebot -- linebot
        |   |
        |   |-- settings.py
        |   |-- urls.py
        |
        |- linebot_cat
            |
            |-- views.py
            |-- urls.py
            |-- api.py
```
:::note warn
触らないファイルは上に記載していません。
:::

### 1. urls.py
:round_pushpin:プロジェクト側の`urls.py`に、アプリ側の`urls.py`へのパスを追加する
```python:linebot/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('linebot_cat.urls')),
]
```

:round_pushpin:アプリ側の`urls.py`にview関数を追加。今回は`linebot_webhook`という名前に設定する
```python:linebot_app/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('linebot_cat/', views.linebot_webhook, name='callback'),
]
```
### 2. views.py

:round_pushpin:`views.py`にview関数を定義する。ここではチャットボットが受け取った画像メッセージを処理をする。
```python:linebot_app/views.py
@csrf_exempt
def linebot_webhook(request):
    if request.method == 'POST':
        req_body = json.loads(request.body.decode('utf-8'))
        for event in req_body['events']:
            if event.get('type') == 'message' and event.get('message', {}).get('type') == 'image':
                reply_token = event['replyToken']
                img_id = event['message']['id']
                handle_image_message(reply_token, img_id)
        return JsonResponse({'status': 'ok'}, status=200)
    return JsonResponse({'status': 'error', 'message': 'Invalid request'}, status=400)
```

以下、メッセージ処理に使うヘルパー関数を明記する。(@linebot_app/views.py)

:one: 処理を行うメインヘルパー関数。:two:、:three:、:four:を内部で呼ぶ。
```python:linebot_app/views.py
def handle_image_message(reply_token, img_id):
    IMG_URL = f'https://api-data.line.me/v2/bot/message/{img_id}/content'
    headers = {'Authorization': f'Bearer {ACCESS_TOKEN}'}
    response = requests.get(IMG_URL, headers=headers)
    img_content = response.content

    is_cat, possibility_text = detect_cat(img_content)
    
    if is_cat:
        text = f'Meow, {possibility_text}'
        translated = translate_text(text)
        if translated:
            text += f' \ {translated}'
        reply_msg = {'type': 'text', 'text':text}
    else:
        text = f'Oops, it\'s not cat! {possibility_text}'
        translated = translate_text(text)
        if translated:
            text += f' \ {translated}'
        reply_msg = {'type': 'text', 'text': text}
    reply(reply_token, [reply_msg])
```

:two: Google Cloud Vision APIによって、猫かどうかを判断する
```python:linebot_app/views.py
def detect_cat(img_content):
    encoded_image = base64.b64encode(img_content).decode('utf-8')
    payload = {
        'requests': [{
            'image': {
                'content': encoded_image
            },
            'features': [{
                'type': 'LABEL_DETECTION',
                'maxResults': 10
            }]
        }]
    }

    response = requests.post(GOOGLE_CLOUD_VISION_API, json=payload)

    if response.status_code != 200:
        print(f'Error Code:{response.status_code}')
        print(response.text)
        return False
    
    response_data = response.json()
    if 'responses' not in response_data or not response_data['responses']:
        print('Invalid response data:', response_data)
        return False

    labels = response_data['responses'][0]['labelAnnotations']
    // 類似度によるメッセージは適当に。
    for label in labels:
        if label['description'].lower() == 'cat':
            if label['score'] >= 0.9:
                text = 'Abusolutely!!'
            elif label['score'] >= 0.8:
                text = 'Certainly!'
            elif label['score'] >= 0.6:
                text = 'Probably'
            elif label['score'] >= 0.4:
                text = 'Maybe?'
            else:
                text = 'Possibly...'
            return True, text
    # もし猫でない場合3つ候補を返信する
    text = f"Is it among {labels[0]['description']}, {labels[1]['description']} or {labels[2]['description']}?"
    return False, text
```

:three: ボットの返信メッセージをDeepL APIを使って、日本語訳する
```python:linebot_app/views.py
def translate_text(text):
    headers = {'Content-Type': 'application/json', 'Authorization': f'DeepL-Auth-Key {DEEPL_AUTH_KEY}'}
    payload = {
        'text': [text],
        'source_lang': 'EN',
        'target_lang': 'JA'
    }
    response = requests.post(DEEPL_API, headers=headers, json=payload)
    if response.status_code == 200:
        response_data = response.json()
        translated = response_data.get('translations')[0].get('text')
        return translated
    return None
```

:four: 画像識別後に、トーク画面にメッセージを返信する
```python:linebot_app/views.py
def reply(reply_token, messages):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + ACCESS_TOKEN
    }
    body = {
        'replyToken': reply_token,
        'messages': messages
    }
    requests.post(REPLY_ENDPOINT_URL, headers=headers, data=json.dumps(body))

```

### 3. api.py
APIエンドポイントや、アクセストークンなどのまとめ

:::note warn
.gitignoreに追加するなどして、リモートリポジトリにあげないように注意
:::

```python:linebot_app/api.py
# LINE
REPLY_ENDPOINT_URL = 'https://api.line.me/v2/bot/message/reply'
ACCESS_TOKEN = '以下説明①'

# Deepl
DEEPL_AUTH_KEY = '以下説明②'
DEEPL_API = 'https://api-free.deepl.com/v2/translate'

# Google Cloud Vision API endpoint
GOOGLE_CLOUD_VISION_API = 'https://vision.googleapis.com/v1/images:annotate?key=以下説明③'
```

#### ① LINE Messaging API アクセストークンの取得方法
最初に登録した[LINE Develpers](https://developers.line.biz/en/)へログインし、トップ > ボット名 > Messaging API設定の下部にある`チャンネルアクセストークン`から取得する
![Screenshot 2024-08-25 at 02.07.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/07b6fbe6-3334-793b-41f7-17038ed8ecf4.png)


#### ② DeepL API アクセストークンの取得方法
[DeepL](https://www.deepl.com/en/translator)にアクセスし、右上の自分の名前 > アカウント > API Keysからキーを生成し取得する。登録方法やAPIの使い方などは[この記事](https://qiita.com/rihok/items/962890b4b86ea052e54c)を参考にしました。
今回のようなチャットボットはフリープランで十分です。
![Screenshot 2024-08-25 at 02.21.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/f9297aac-ed66-0eae-d824-f998f081c1b9.png)

#### ③ Google Cloud Vision API アクセストークンの取得方法
:one: APIを利用するために、まずOAuthクライアントIDを作成します。[この記事](https://qiita.com/t-mochizuki/items/ab4a0b2f70426f73e1b6)通りに進めます。
:two: [Cloud Vision API](https://console.cloud.google.com/apis/credentials/key/c31d4930-468b-4140-b04b-c1c0b1776617?hl=ja&project=wide-planet-430708-c3)にアクセスし、画面中央の`+認証情報を作成`をクリックし、`APIキー`を選択します
![Screenshot 2024-08-25 at 02.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/0dbfb335-3fdf-a13f-2ec7-f77d28d2b6bf.png)
生成されたAPIキーを選択し、
![Screenshot 2024-08-25 at 02.43.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/a8f2d8d6-4898-5770-d617-77ccfcf8d8a1.png)
詳細情報 > API Keyから取得する
![Screenshot 2024-08-25 at 02.37.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/5e9ff022-8bfb-c2e3-966e-a8c6be5b6557.png)

実装はこれにて終了:beer:

# ngrokでチャットボットを動かしてみる
#### ① [ngrok公式](https://ngrok.com/download)から、該当するファイルをダウンロード
ダッシュボードからYourAuthTokenを取得し、以下のコマンドを当プロジェクトフォルダにて実行します。
```
ngrok config add-authtoken YOUR_AUTHTOKEN
ngrok http 8000
```
以下のような情報がターミナル上に表示されたらngrokの起動成功です。
![Screenshot 2024-08-26 at 02.11.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/834e2a80-bdac-9035-0749-1ee155f31b8a.png)

#### ② settings.pyにホストを追加
起動したngorkがDjangoプロジェクトにアクセスできるようにします
上の画像、黄色で塗られているForwarding > https://　の右部分を追加する　（ -> の前まで）
```python:linebot/settings.py
ALLOWED_HOSTS = ['黄色部分.ngrok-free.app']
```

#### ③ LINE Developers側にもホストを追加
[LINE Develpers](https://developers.line.biz/en/)へログインし、トップ > ボット名 > Messaging API設定 > Webhook URLにも同じようにホストを追加

上の画像、黄色で塗られているForwarding > https://　の右部分　＋　/linebot_cat(Djangoプロジェクトのアプリ名）
![Screenshot 2024-08-26 at 03.11.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/08ecd084-1d66-0576-093e-7ac8b07855ab.png)

#### ④ 最後にDjangoプロジェクトの開発サーバーを立ち上げて、ボットを使ってみる
```
./manage.py runserver
```

日本語訳が不自然なのはツッコまないでください

猫の場合
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/96df1424-b2ca-a264-e1d4-6d90420d14b3.png" width=75%>

以下、猫以外ならば３つの候補を提示します
└ Google Cloud Visionの結果から、類似度の高い順

犬の場合
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/6e77a6fc-989c-f316-9a44-bae4c77d2d1c.png" width=75%>

ラーメンの場合
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761771/14a08cdf-72a0-fef1-8cab-d8f53d422e87.png" width=75%>

# GitHub
https://github.com/sungwi/linebot_cat.git