Raspberry Pi 3B+でEmpath APIを使用し、声から感情を分析してみました

# Empathとは
https://webempath.net/lp-jpn/
> 音声等の物理的な特徴量から気分の状態を独自のアルゴリズムで判定するプログラム

人の声から、その人の感情を数値化してくれるプログラムです。APIが公開されていますので、今回はそれを利用します。

# 利用しようと思った理由
「人の感情（特に怒り）を分析し、その結果を見える化する」という取り組みで、声から感情を分析できるEmpath APIを利用することになりました。

# システム構成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/321300/de5c9c95-66d1-0306-7209-2bb64c09d6af.png)
（怒った）人の声→ 音声を拾ったRaspberry Pi Zero
→ 本記事のRaspberry Pi 3B+
→ ESP32→ 角（フルカラーで光る）
になります。
本ページで扱うのは「WiFi(プロトコルはhttp）で音声ファイルを受け取り、Empath APIをコールし、数値化された感情をRGBの数値に変換する。」までになります。

# Empath APIを使うための事前準備
1. API Keyの取得  
    1. EmpathのWebサイトへアクセス  
    再び  
    https://webempath.net/lp-jpn/  
    にアクセスします。  
    画面に「WEB Empath API公開中！」という部分があるので、クリックします。  
    1. ログイン  
    登録したメールアドレスとパスワードを入力し、[ログイン]をクリックします。
    1. API Key設定画面  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/321300/050f5974-17a8-33a5-3a3d-0a93b3ea928a.png)  
    このような画面が表示されるので、[API Key設定]をクリックします。  
    画面右側に[追加]ボタンが表示されるので、[追加]をクリックします。
    1. アプリケーション用のAPI Key発行  
    APIキーのタイトル入力画面が表示されるので、アプリケーション名を入力し、[OK]をクリックします。
    1. API Keyを控える  
    画面右側にAPIキーが表示されるので、控えておきます。Empath APIへのリクエストの送信で使用します。
1. ドキュメントの取得  
    [ドキュメント]をクリックし、Empath APIの仕様書（PDF)をダウンロードします。  
    ここにEmpath APIのURLが記載されています。

# 環境構築
Empath APIを利用するため、各種セットアップを実施します。

## Raspberry Piの環境
- ハード：
Raspberry Pi 3B+
- OS：
Raspbian Stretch with desktop(ubuntu)

## Python2.7のインストール
```
apt-get install python2.7
```

Empath APIはhttpsでリクエストが送信できれば、言語は問いません。今回はサンプルがPython2.7なので、それを使います。

## pipのインストール
```
apt-get install python-pip
```

## poster(version 0.8.1)のインストール
posterはhttpで音声ファイルをアップロードするのに使用します。
今回のシステムではEmpath APIへリクエストする際、音声ファイルを送信するため使用しました。

```
pip install poster==0.8.1
```
サンプルでposterのバージョンが指定されていたので、バージョン指定でposterをインストールします。

## Djangoのインストール
DjangoはWebアプリケーションフレームワーク、かつWebサーバーとして利用が可能です。
今回のシステムでは音声認識（Raspberry Zero W)から音声ファイルをhttpで受信するのに使用します。

```
pip install django
```
結果…怒られます。

```shell-session
Downloading/unpacking django
  Downloading Django-2.1.4.tar.gz (8.6MB): 8.6MB downloaded
  Running setup.py (path:/tmp/pip-build-FTlLGX/django/setup.py) egg_info for package django
    
    ==========================
    Unsupported Python version
    ==========================
    
    This version of Django requires Python 3.5, but you're trying to
    install it on Python 2.7.
    
    This may be because you are using a version of pip that doesn't
    understand the python_requires classifier. Make sure you
    have pip >= 9.0 and setuptools >= 24.2, then try again:
    
        $ python -m pip install --upgrade pip setuptools
        $ python -m pip install django
    
    This will install the latest version of Django which works on your
    version of Python. If you can't upgrade your pip (or Python), request
    an older version of Django:
    
        $ python -m pip install "django<2"
    Complete output from command python setup.py egg_info:
```

最新のDjangoを入れるには、Python2.7のpipでは古い、と…
仕方がないので、Djangoの古いバージョン（1.11）をインストールします。

```
pip install "django<2"
```
# 作り込み
## Djangoの設定
Djangoの設定やサービスの立ち上げ方は
https://qiita.com/tfrcm/items/bff55a0b8ae1b76c0ca1
などを参考にして下さい。

## 音声ファイル(http)の受付部分
単体テスト用にGETでリクエストが来た場合、アップロードページへ遷移します。
POSTでリクエストが来た場合、Empath APIへのリクエスト送信と感情判定を実施します。
音声ファイル（http）の受付部分で要求するパラメータは
api-key=*Empath APIの取得したAPIキー*
file=*解析対象の音声データファイル*
になります。

```python:views.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from __future__ import print_function

from django.shortcuts import render
from django.http import HttpResponse
from upload.models import FileNameModel
import os
import threading
from tasks import voice_rgb

UPLOADE_DIR = os.path.dirname(os.path.abspath(__file__)) + '/static/files/'
TARGET_FILE = ''


def form(request):
    if request.method != 'POST':
        return render(request, 'upload/form.html')

    apikey = request.POST['api-key']
    upload_file = request.FILES['file']
    path = os.path.join(UPLOADE_DIR, upload_file.name)
    destination = open(path, 'wb')

    for chunk in upload_file.chunks():
        destination.write(chunk)

    # 結果をDBに保存
    insert_data = FileNameModel(file_name=upload_file.name)
    insert_data.save()

    # ファイルアップロード後、Empathにファイルを送信し、結果を受け取る
    print("upload done.")
    t = threading.Thread(target=voice_rgb, args=(path, apikey, ))
    t.start()

    return HttpResponse(status=200)
```

スレッドを生成し、スレッド側で`voice_rgb`(Empath APIのコールと感情の判定)を実行します。
解析に時間がかかる可能性を考慮し、非同期としています。（実際は、Empath APIのレスポンスは非常に早いため同期処理でも問題ありません。）

- 単体用のテストページ  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/321300/f6ed10a9-702a-5374-bae8-5a25933317ff.png)  
api-keyには事前準備で控えたAPIキーを入力します。  
ファイル選択でEmpath APIに送信する音声ファイルを選択します。

## Empath APIのコールと解析部分
```python:tasks.py
# -*- coding: utf-8 -*-
from poster.encode import multipart_encode, MultipartParam
from poster.streaminghttp import register_openers
import urllib2
import json
import time
import subprocess

MAX_RANGE=255
SLEEP_TIME=1
EMPATH_URL="Empath APIのマニュアルにあるURL"
MAC_ADDR="ESP32のMACアドレス"
HANDLE="ESP32のBLEの書き込み先ハンドル"

# 解析メソッド
def max_func(n):
    return n[1]

def voice_rgb(file_path, apikey):
    time.sleep(SLEEP_TIME)
    register_openers()
    items = [MultipartParam('apikey', apikey),
             MultipartParam.from_file('wav', file_path)]
    datagen, headers = multipart_encode(items)
    request = urllib2.Request(EMPATH_URL, datagen, headers)
    readObject = urllib2.urlopen(request)  # type: object

    if readObject.getcode() != 200:
        print("HTTP status %d" % (readObject.getcode()))

    response = json.load(readObject)
    print(response)

    if response['error'] != 0:
        print("analyze error %d" % (response['error']))
        return

    list=[('anger', response['anger']),('joy', response['joy']),('sorrow', response['sorrow']),('calm', response['calm'])]
    max_num=max(list, key=max_func)
    red=(response['anger']+response['energy']/2)*(MAX_RANGE//50)
    if red > 255:
        red = 255
    green=(response['joy']+response['energy']/2)*(MAX_RANGE//50)
    if green > 255:
        green = 255
    blue=(response['sorrow']+response['energy']/2)*(MAX_RANGE//50)
    if blue > 255:
        blue = 255
    rgb=(red<<16)+(green<<8)+blue

    print("rgb=", rgb)

    while True:
        if subprocess.check_call(["gatttool","-b",MAC_ADDR,"--char-write-req","-a",HANDLE,"-n", format(rgb, 'x')]) == 0:
            break
```
Empath APIからは「calm/angry/joy/sorrow/energy」の5要素が数値（0〜50）で返ってきます。
レスポンスのフォーマットはJSONになります。
実際の実行結果は
`{u'joy': 2, u'sorrow': 10, u'calm': 27, u'error': 0, u'anger': 9, u'energy': 1}`
このようなになります。
解析結果出力側のESP32にはRGB（各1バイトの合計3バイト）でデータを渡します。
そのため、
R：(angryの数値 + energyの数値/2)\*(255/50)
G：(joyの数値 + energyの数値/2)\*(255/50)
B：(sorrowの数値 + energyの数値/2)\*(255/50)
で各色計算し、最後に3バイトに合成しています。
energyを計算式に入れたのは、「energyの数値が高い = 感情の度合いが強い」と考えたためです。
先程のレスポンス結果のRGB数値は
「R：0x2D」、「G：0x0A」、「B：0x32」で`('rgb=', 2951730)`になります。（16進数では0x2D0A32）
※BLEへの書き込み部分が入っていますが、そこの説明は省略します。

# 苦労した（するだろう）箇所...
Empath APIをコールするところ自体はそこまで難しくありませんでした。
苦労したのは単体テストに使用する音声ファイルの作成です。（システム全体を通しての場合も、録音部分の方が苦労してました…）
Empath API（お試し）では「録音時間が5秒未満」の制限があり、短くても長くてもエラーになります。程よい長さの音声ファイルの作成が難しいです。単純に音声ファイルを秒数でカットすると、話している言葉の途中を切り出す事になるためか、解析結果の数値が意図したものと全く違う数値になる事がありました。

# 今後...
公開されているお試しAPIは音声ファイルの送信で感情を判定しますが、製品版ではリアルタイムで感情を分析しているようです。
リアルタイム版もお試し版が出たら、また試してみようと思います。