[Translator Musk]翻訳機能付きマスクの開発 

## モチベーション

昨年のマスク×テクノロジーブームの中で自分も何か作りたいなと思い立ちました。
何作ろうかなと考えたときに思いついたのがドラえもんのひみつ道具「ほんやくコンニャク」。

　　![ほんやくコンニャク](https://www.tv-asahi.co.jp/doraemon/contents/tool_ha/0001/img/1_1.jpg)

これマイクとディスプレイあればつくれそうだなというノリで翻訳機能付きマスクを作りましたので参考にしたページの備忘録。
ついでに表情がわかりにくいという問題を解決するために発話から感情推定する機能も付けます。

## 成果物

完成品はこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234113/b92c6c7e-b324-387a-d031-3901447e6c59.png)


arduinoのコードはgitにあげています。ウェブアプリと翻訳・感情解析はglitchです。

[arduinoコード](https://github.com/ma-ring/yohaku/tree/main/TranslatorMask)
[ウェブアプリ](https://translatermask.glitch.me/)
[翻訳・感情解析API](https://transmask-api.glitch.me/)

開発環境

* Windows10
* ESP32 + arduino
* glitch 

## 設計方針

### 機能要件

基本機能としては以下の2つ。

 1. マイクを搭載して発話を認識
 1. 発話を翻訳（とりあえず日本語から英語に翻訳）
 1. 英語から感情判定
 1. ディスプレイを搭載して翻訳と感情推定の結果を表示 

### システム図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234113/f9893d8b-50f4-3432-9249-d0d5efe60608.png)

クロスプラットフォーム対応のため外部アプリケーションはウェブアプリにしました。
スピーカーは今回搭載してないです。

## マスク型デバイス

### デザイン

マスク本体は3Dプリンターで出力し、ゴムの耳掛けをつけました。
マイクとマイコンはセンターに、ディスプレイは左右に2個ついてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1234113/67cc320a-164f-ef01-8c3b-060175d7aadb.png)
 

### マイコン開発

#### 使用機材

 * [ESP32](https://www.amazon.co.jp/gp/product/B0718T232Z/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
 * [ILI9341](https://www.amazon.co.jp/gp/product/B072N551V3/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)

#### 液晶への表示部分

日本語を表示できる＆処理速度が速いということで、描画にはをTFT-eSPIを使用。
また複数の液晶を使うための方法はKitecraft氏のGitを参考にさせてもらいました。

* [TFT-eSPI](https://github.com/Bodmer/TFT_eSPI)
* [Multiple_TFT_Displays](https://github.com/kitecraft/Multiple_TFT_Displays)

```arduino:translatorMask.ino

#include "Free_Fonts.h"
#include "SPI.h"
#include "TFT_eSPI.h"

TFT_eSPI tft = TFT_eSPI();
#define FIRST_SCREEN 5
#define SECOND_SCREEN 27

void setupTFT(){
  pinMode(FIRST_SCREEN, OUTPUT);
  digitalWrite(FIRST_SCREEN, HIGH);
  pinMode(SECOND_SCREEN, OUTPUT);
  digitalWrite(SECOND_SCREEN, HIGH);
  digitalWrite(FIRST_SCREEN, LOW);
  digitalWrite(SECOND_SCREEN, LOW);
  
  tft.init();
  tft.setFreeFont(FF4);
  tft.setTextSize(1);
  digitalWrite(FIRST_SCREEN, HIGH);
  digitalWrite(SECOND_SCREEN, HIGH);
}

void setup() {
  setupTFT();
}

void loop() {
  // put your main code here, to run repeatedly:

  //draw on 1st display
  digitalWrite(FIRST_SCREEN, LOW);
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);
  int line = displayStr.size() / FONT_NUM_MAX;
  tft.drawString("1st screen,10,10);
　digitalWrite(FIRST_SCREEN, HIGH);
      
  //draw on 2nd display
  digitalWrite(SECOND_SCREEN, LOW);
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
　tft.drawString("2nd screen",10,10); 
  digitalWrite(SECOND_SCREEN, HIGH);

}

```

最初にFIRST_SCREENとSECOND_SCREENにそれぞれに使うピンを指定します。
1stスクリーンに描画する際はFIRST_SCREENをLOW、SECOND_SCREENをHIGH
2ndスクリーンに描画する際はFIRST_SCREENをHIGH,SECOND_SCREENをLOw 
にして描画することで片方だけに描画できます。  

#### 外部アプリケーションとの通信

通信にはLBTを使用しました。
マスク側をperipheral、webアプリ側をcentralにします。

```arduino:translatorMask.ino
#include "BLEDevice.h"

static String DEVICE_NAME = "TranslatorMask";
static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
static BLEUUID    charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");
BLECharacteristic *pCharacteristic;   // キャラクタリスティック
bool deviceConnected = false;           // デバイスの接続状態
bool bInAlarm  = false;                 // デバイス異常判定

//SET UP BLE /////////////////////////////////
//callback for server 
class functionServerCallBacks : public BLEServerCallbacks{
  void onConnect(BLEServer* pServer){
    //Serial.println("BLE CONNECTED");
  }
  void onDisconnect(BLEServer* pServer){
    //Serial.println("BLE DISCONNECTED");
  }
};
//callback for characteristic
class functionCharaCallBacks : public BLECharacteristicCallbacks{
  void onRead(BLECharacteristic* pCharacteristic){
    //Serial.println("read");
  }
  void onWrite(BLECharacteristic* pCharacteristic){
    std::string str = pCharacteristic->getValue();
    displayStrJ  =  str;
  }
  
};


//setup
void setupBLE(){
  BLEDevice::init("TRANSLATOR MASK");
  //Create server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new functionServerCallBacks());
  //Create service
  BLEService *pService = pServer->createService(serviceUUID);
  //Create characteristics
  pCharacteristic = pService->createCharacteristic(charUUID, 
                              BLECharacteristic::PROPERTY_READ   |
                              BLECharacteristic::PROPERTY_WRITE  |
                              BLECharacteristic::PROPERTY_NOTIFY |
                              BLECharacteristic::PROPERTY_INDICATE);
  pCharacteristic->setCallbacks(new functionCharaCallBacks);
 
  //Start service
  pService->start();
  //Start advertising
  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->start();
}


void setup() {
  // put your setup code here, to run once:
  //Serial.begin(9600);
 
  setupBLE();
}

```

functionServerCallBacksのonConnect/onDisconnectで、接続時の処理を定義します。
functionCharaCallBacksのonRead/onWriteでread/writeイベントを定義します。
今回はwebアプリからwriteでディスプレイに表示する文字を送るようにしました。

#### 配線



## 外部アプリケーション

先述の通りウェブアプリで開発する方針にしたのでglitchで開発。
そもそもウェブアプリでLBTできるのか？と思ったんですが、Chromeだと提供されてました。
調べるとUSBとかも認識できるらしくさすがChrome、ただほかのブラウザだと鄭経れてない機能が多かったです。

翻訳と感情解析はpythonのライブラリを使用しました。
ユーザーが使用するwebアプリ(javascript)と翻訳&感情解析api(python)の2つのアプリが存在する感じになります。
統合できる気もするんですがとりあえず今回は2つに分けました。

ただ使用したglitchはjs+htmlのためのサービスなので、python使うにはちょっと工夫がいります。

### glitchでpythonを使う準備

glitchで新規にwebアプリを作成し、glitch.jsonを以下に書き換えます。
またrequirements.txtとserver.pyを新規作成します。

glitch.jsonでpythonを使うための設定を書き、requrements.txtにはpythonで使いたいライブラリ(今回はFlask）を記入します。
実際のpythonでの処理はserver.pyに記入します。

```json:glitch.json
{
  "install": "pip3 install --user -r requirements.txt",
  "start": "PYTHONUNBUFFERED=true python3 server.py",
  "watch": {
    "ignore": [
      "\\.pyc$"
    ],
    "install": {
      "include": [
        "^requirements\\.txt$"
      ]
    },
    "restart": {
      "include": [
        "\\.py$",
        "^start\\.sh"
      ]
    },
    "throttle": 1000
  }
}

```


```text:requirements.txt
Flask
```

### 音声認識と翻訳

SpeechRecognition を使用します
ボタンを押したらスタートするように実装しました

*[Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition
](https://qiita.com/hmmrjn/items/4b77a86030ed0071f548)

```Javascript:script.js
window.SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition;
const recognition = new window.SpeechRecognition();

var intervalIDtranslate; 
var startedRecognition = false;
recognition.interimResults = true;
recognition.continuous = true;

//speech recognition
function startRecognition(){
  if(!startedRecognition){
    recognition.start();
    intervalIDtranslate = setInterval(translate, SEND_DATA_INTERVAL);
    (document.getElementById("startRecognition")).disabled  = true;
    startedRecognition = true;
  }
}
//this event will be called when speech finish
recognition.onresult = (event) => {
  inputText.textContent =  "";
  //for(var i = 0; i < event.results.length; i++){
      //console.log(event.results[i]);
      for(var j = 0; j < event.results[speechResultIndex].length; j++){
        inputText.textContent += event.results[speechResultIndex][j].transcript;
      } 
      if(event.results[speechResultIndex].isFinal){
         speechResultIndex ++;
         //console.log("final");
      }
  //}
}
recognition.onend = (event) => {
  startedRecognition = false;
  speechResultIndex = 0;
  clearInterval(intervalIDtranslate);
  (document.getElementById("startRecognition")).disabled  = false;
  console.log("end");
}

```

### 翻訳

gooleによるTranslatorを使用します。
Flaskを使い、/trans宛にPOSTで送られてきた文字を翻訳します。

```python:server.py
from flask import Flask, request, make_response,jsonify
from googletrans import Translator

app = Flask(__name__)

@app.route("/trans", methods = ['POST'])
def translate():
  data = json.loads(request.data.decode('utf-8'))
  message = data['message']
  
  //翻訳する文章がない場合は
  if message == '':
    return jsonify({'result':''})
  
  //翻訳し、結果をjsonで返す
  trans = translator.translate(message)
  return jsonify({'result':trans.text)

@app.after_request
def after_request(response):
  response.headers.add('Access-Control-Allow-Origin', '*')
  response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return response

if __name__ == "__main__":
  app.run()

```

### 感情判定

感情分析にはpythonのライブラリであるnltkを使用しました。
nltkは英文に対する感情解析なので、翻訳結果が英語の時のみ使用します。
解析結果emotionは-1から1の値で返され、-1に近いほどnegative、1に近いほどpositiveになります。
今回はとりあえず以下のように割り振りました。

-1.0 ≦ motion < -0.7 : sad
-0.7 ≦ motion < -0.3 : angry
-0.3 ≦ motion <  0.3 : neutral
 0.3 ≦ motion <  0.7 : joy
 0.7 ≦ motion ≦ 1.0 : happt


```python:server.py

from flask import Flask, request, make_response,jsonify
from googletrans import Translator
import json

import nltk
nltk.download('vader_lexicon')
from nltk.sentiment.vader import SentimentIntensityAnalyzer

app = Flask(__name__)
translator = Translator()
vaderAnalyzer = SentimentIntensityAnalyzer()

@app.route("/trans", methods = ['POST'])
def translate():
  data = json.loads(request.data.decode('utf-8'))
  message = data['message']
  emotion = 2
  
  if message == '':
    return jsonify({'result':'','emotion':emotion})
  
  trans = translator.translate(message)
  if trans.dest == 'en':
    scores = vaderAnalyzer.polarity_scores(trans.text)
    if scores['compound'] < -0.7 :
      emotion = 0
    elif scores['compound'] < -0.3:
      emotion = 1
    elif scores['compound'] < 0.3:
      emotion = 2
    elif scores['compound'] < 0.7:
      emotion = 3
    else:
      emotion = 4
  #print(res.text)
  return jsonify({'result':trans.text, 'emotion':emotion})

@app.after_request
def after_request(response):
  response.headers.add('Access-Control-Allow-Origin', '*')
  response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return response

if __name__ == "__main__":
  app.run()

```


## まとめ

以上、翻訳機能付きマスクデバイスの開発備忘録でした。
普段使わない言語とかを使うのはやっぱり面白いですね。
今後は感情に合わせて絵文字を表示とかやってみたい。

---
参考文献

 - [ひみつ道具カタログ](https://www.tv-asahi.co.jp/doraemon/tool/ha.html) : テレビ朝日