🙆

感情に合わせて適切な相槌を打ってくれるアプリ~STEP1~

2022/02/13に公開

モチベーション

昨今の流行病の影響でリモートワーク・お家時間が主流となり、中々人に会う機会が減りました。家で一人で過ごすためのコンテンツは世にあふれているため、一人で過ごす時間がとても長くなりました。そして私はあることに気が付きました。人とお話できなくて寂しいと。
コミュニケーションは相互作用であり、自分が話したことに対して相手が何かしらのリアクションをしてくれることで盛り上がります。一人で話していても何のリアクションも返ってこないので、中々むなしいです。(なので、ひとりで滔々としゃべり続けられるYoutuberの方々は本当にすごいと思います。)
ということはもし一人で話していたとしても、何かしらリアクションが返ってくれば、一人で話すよりも寂しくないのでは?と思い、そんなアプリを作ってみたいと思います。

使用技術

  • Python 3.6.9
  • pyaudio 0.2.11
  • flask 2.0.2
  • Docker version 19.03.6, build 369ce74a3c
  • javascript

作戦

いきなり理想形を創ろうとしても挫折するのが目に見えています。
そのため理想形は考えつつ、スモールステップでつくっていきます。

※下記記事を参考に、まずは「小さくはじめる」というスタンスです。
https://www.linkcom.com/blog/2021/10/miseban2.html

理想形

「画像・音声を読み取って感情を分析し、適切な相槌を返す」

完成イメージは以下です。

※画像読取の参考イメージ
https://emotion-ai.userlocal.jp/face

理想形を実現するために必要な機能は以下です。

  1. カメラから画像を読み取る
  2. 読み取った画像から感情を数値化する
  3. マイクから音声を読み取る
  4. 読み取った音声から話の内容を把握する
  5. 算出した感情と話の内容を加味して、適切な返答を返す

...はい。とても遠い道のりです。
このままだとだんだん心がすり減っていて、完成したころにはネガティブ判定しか出なくなってしまいます。
ということで、ゴールを細かく設定します。

スモールステップ

以下のようなステップで作ってみます。
まずは動くものを順々に作ってみます。

  1. 音声が検知されたら、話を聞いているうなづきGifを表示する
  2. 音声が途切れた時に、簡単な相槌を発音させてみる
  3. 音声を都度自然言語処理し、都度感情分析値を画面に出してみる
  4. 感情分析の数値に合わせて、相槌を変えてみる。
  5. カメラから取得した映像を画像認識し、都度感情分析の数値を画面に出してみる
  6. 映像、音声の感情分析値を加味して、相槌を変えてみる

開発開始!

音声が検知されたら、話を聞いているうなづきGifを表示する

やりたいことは以下です。

  1. Webアプリでひな形を作る
  2. マイクから音声を検知する
  3. 音声を検知したらGifを変更する

1. Webアプリでひな形を作る

まずはページを表示する。話はそれからだ。

ゴールの感情分析はPythonのコードで書かれているものが多かったため、フロントもPythonを使います。(ちなみに私は初心者です。)

PythonのWebフレームワークの記事をいくつか見て、Flask で開発することにします。
https://and-engineer.com/articles/YWFRChEAACMA-r03
https://myafu-python.com/knowledge/brython/
https://dividable.net/programming/python/python-web-aplication-development

早速Githubに空リポジトリを作りました。
https://github.com/km42428/give-me-response-front/commit/af5de04d4a203596781416e221aa0b3c4a3498af
(名前が欲しがりな感じがしてちょっと恥ずかしいですね笑)

さっそくチュートリアルに従って、Flackをインストールします。
https://flask.palletsprojects.com/en/0.12.x/installation/#installation
https://flask.palletsprojects.com/en/0.12.x/quickstart/

※Python3系で実装したかったため、pip -> pip3python -> python3 で進めます。

$ pip3 install Flask

インストール出来たら、サンプルコードを作成します。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

そしてPythonファイルをFlaskで実行します。

$ export FLASK_APP=index.py
$ flask run

そして、無事に表示されました。

ふむふむ。順調です。

このコードを元に、webページを作ります。
画面遷移は特になく1ページだけ作ります。

レイアウト作成

ここで壁にぶち当たります。私にはフロントのデザイン力がほぼないのです。
デザイン戦闘力5の私はどうすれば。。このままではラディッツにやられてしまいます。

CSSを自分で書くのは地獄なので、ここは大人しく巨人の肩に乗ります。
flaskとBootstrapを組み合わせた記事を参考にします。
bootstrapを使うと、htmlのクラス名を適切に指定するだけでデザインが作れるので最高です。
https://engineer-lifestyle-blog.com/code/python/flask-bootstrap-css-implementation/

公式ドキュメントはこちら
https://getbootstrap.jp/

※頑張れる方はこの辺を参考にデザインテンプレートを使うと良いですね
https://itips.krsw.biz/how-to-apply-responsive-css-template-flask/

bootstrapを有効化するため、インストールします。

$ pip3 install flask_bootstrap

フォルダ構成は以下のようにします。

.
├── index.py // 実行ファイル
├── start.sh // 毎回起動コマンドを打つのが面倒なので、スクリプト化
├── static // 静的ファイル格納
│   └── img
│       └── unazuki.gif
└── templates // Flask用テンプレート格納
    └── index.html

諸々設定後、下記のようにひたすら頷いてくれるサンプルができました。


うーん、これだけでも少し寂しさを埋められそうです。

※gif画像引用元:
https://tenor.com/view/fumufumu-unun-unazuki-ryoukai-gif-11690039

2. マイクから音声を検知する

ただ頷く画像を見るだけでも一人ぼっちよりはましですが、やはりこちらのアクションに応じて動きが変わると、さらに臨場感が出ます。
ということでマイクの音声を取得して変化を生んでみます。

Pythonでマイク音声を取得する (WSL)

Python マイク入力 で検索すると以下の記事が見つかりました。
https://niyanmemo.com/335/

この中で紹介されていた PyAudio を利用してみます。

Windows端末でPyAudioのインストールに失敗する記事がたくさんあったので、うまくいっている人を参考にします。
https://kabukawa.hatenablog.jp/entry/2018/12/18/134840

諸々必要なモジュールをインストールします。

$ sudo apt-get install portaudio19-dev
$ pip3 install pyaudio

そしてサンプルファイルを実行すると、以下のエラーが出ました。

...
Traceback (most recent call last):
  File "sample.py", line 27, in <module>
    stream_callback=callback)
  File "/home/moriyama-k/.local/lib/python3.6/site-packages/pyaudio.py", line 750, in open
    stream = Stream(self, *args, **kwargs)
  File "/home/moriyama-k/.local/lib/python3.6/site-packages/pyaudio.py", line 441, in __init__
    self._stream = pa.open(**arguments)
OSError: [Errno -9996] Invalid output device (no default output device)

どうやら、WSLでは出力デバイスが特定されていないようです。
以下の記事でうまく回避できたようなので参考にします。
https://kabukawa.hatenablog.jp/entry/2018/12/18/134840

WSL側で pulseaudio クライアントをインストール

$ sudo apt install pulseaudio 

うーん。うまく動かない。。

ということで諦めてMacに切り替えます。

Pythonでマイク音声を取得する (Mac)

Macでのpyaudioインストールはこちらの記事を参考にしました。

https://mayrsblog.com/pyaudio/

$ brew install portaudio
$ pip install pyaudio

これらをinstallの上、下記記事・リポジトリを参考にサンプルコードを作成しました。
https://takeshid.hatenadiary.jp/entry/2016/01/10/153503
https://github.com/imajoriri/finger-snap

import pyaudio
import numpy as np

CHUNK = 1024
RATE = 44100
p = pyaudio.PyAudio()

stream = p.open(format=pyaudio.paInt16,
                channels=1,
                rate=RATE,
                frames_per_buffer=CHUNK,
                input=True,
                output=True)  # inputとoutputを同時にTrueにする

all = []
def is_talking(array):
  return any(list(array))

# tmpは常に同じ長さ
tmp = [False for k in range(0, 20)]
while stream.is_active():
    input = stream.read(CHUNK)
    npData = np.frombuffer(input, dtype="int16") / 32768.0

		# 声の大きさが閾値を超えると話している判定にする
    threshold = 0.1
    isThresholdOver = False
    if max(npData) > threshold:
        isThresholdOver = True
    
    tmp.append(isThresholdOver)
    tmp.pop(0)
    if (is_talking(tmp)):
      print("お話し中...")
    else:
      print("おしゃべりしましょ")

stream.stop_stream()
stream.close()
p.terminate()

print("Stop Streaming")

こちらを実行すると、
マイクで音声を取得すると "お話し中..."
音声が小さいと "おしゃべりしましょ"
が出るようになります。

3. 音声を検知したらGifを変更する

1.Flaskレイアウトと2.マイクから音声を検知するを組み合わせて、アプリを作ります。

ここで躓いたポイントを共有します。

...
# トップページ
@app.route('/')
def hello_world():
    return render_template("index.html", is_talking=is_talking)
...

初めの着想では、刻々と更新されるis_talkingが都度templateへ渡されると考えていました。
しかし、よくよく考えると / はFlaskで作成したAPIであり、リクエストした一回しか値を渡せない気がしてきました。(本当は都度値を渡す方法があるのかもしれませんが、Flask歴1日の私にはわかりませんでした)

ということで、templateに値を直接渡すのは諦めて、別のAPIに切り出してそこから値を取ることにします。

...
# 発音状態を確認する
@app.route('/audio')
def get_audio():
    return dict(is_talking=is_talking(tmp))
...

is_talking は上部に定義をしておき、APIリクエストされた時点の値を返すことにします。

APIリクエストは、templateの中で javascript で記載します。

...
<!-- 一定時間毎に発音状態を確認する -->
<script type="text/javascript">
  const noddingSrc = "static/img/nodding.gif";
  const waitingSrc = "static/img/waiting.gif";
  let nowSrc = noddingSrc;
  const check = () => {
    const request = new XMLHttpRequest();
    request.open('GET', 'http://localhost:5000/audio', true);
    request.responseType = 'json';
    request.onload = function () {
      const data = this.response;
      const img = document.getElementById('gif');
      if (data.is_talking && nowSrc !== noddingSrc) {
        img.setAttribute('src', noddingSrc);
        nowSrc = noddingSrc
      } else if (!data.is_talking && nowSrc !== waitingSrc) {
        img.setAttribute('src', waitingSrc);
        nowSrc = waitingSrc
      }
    };
    request.send();
  }
  setInterval(check, 200);
</script>
...

setInterval(callback, interval) で定期的に /audio へリクエストを送ります。

上記の構成で音声に従ってgifを入れ替えるアプリが完成しました。

zennの仕様で音は入れられませんが、話すとgifが切り替わるサンプルができました。
私が声を出している間、猫が頷いてくれます↓

ここまでのコードはこちら。
https://github.com/km42428/give-me-response-front/tree/v1.0.0

※最終的なインストールモジュールは以下です。

absl-py==0.6.1
APScheduler==3.8.1
astor==0.7.1
asyncio==3.4.3
backports.zoneinfo==0.2.1
click==8.0.3
cycler==0.10.0
dataclasses==0.8
dominate==2.6.0
Flask==2.0.2
Flask-Bootstrap==3.3.7.1
gast==0.2.0
grpcio==1.16.1
h5py==2.8.0
importlib-metadata==4.8.3
importlib-resources==5.4.0
itsdangerous==2.0.1
Jinja2==3.0.3
Keras-Applications==1.0.6
Keras-Preprocessing==1.0.5
kiwisolver==1.0.1
Markdown==3.0.1
MarkupSafe==2.0.1
matplotlib==3.0.2
numpy==1.15.4
pandas==0.23.4
protobuf==3.6.1
PyAudio==0.2.11
pyparsing==2.3.0
python-dateutil==2.7.5
pytz==2018.7
pytz-deprecation-shim==0.1.0.post0
six==1.11.0
tensorboard==1.12.0
tensorflow==1.12.0
termcolor==1.1.0
typing_extensions==4.0.1
tzdata==2021.5
tzlocal==4.1
visitor==0.1.3
Wave==0.0.2
Werkzeug==2.0.3
zipp==3.6.0

まとめ

まずは音声に従って動きを変えるアプリを作成しました。
pyaudioを使えば音声検知や学習済みモデルで感情分析もできそうです。
引き続き開発をしてみます。

Discussion