kubernetes上のNode-REDで音声対話サービスを実現してみる

# はじめに
我が家のホームアシスタント(勝手にそう呼んでいる)は、時報や天気をお知らせしてくれるし、雨雲が接近したら発話で注意を促したり、デバイスの操作時にも可愛い声でお喋りしてくれます。
そんなホームアシスタントにリップシンクで喋ってくれる3Dアバターを受肉させたのですが、なんだか可愛く思えてきて目や耳を持たせたくなりました。(以下はアバター開発風景)

https://twitter.com/shikasey/status/1460909810203824129?s=21

耳は、Raspberry Piでは昔から音声認識として『[Julius](https://julius.osdn.jp/)』というソフトが使われているようなので、コンテナ化してNode-REDから使おうと考えました。認識率を高めるための辞書のカスタマイズなどもする予定ですが、とりあえず基本部分の構築ができたので備忘録も兼ねて記事にします。

https://julius.osdn.jp/

# 前提環境
- Raspberry Pi 4 Model B * 7台 (kubernetes v1.22.0)
- RaspiOS (buster) 64bit (kernel 5.10.52-v8+)
- Node-RED v2.1.1 (自作コンテナ)
- バッファロー USB Webカメラ BSWHD06MWH

# 構築

## USBマイクの接続
juliusをデプロイするノードのUSBポートにマイク(Webカメラのマイクでも可)を接続します。マイクはモノラル16bitで16kHzが望ましいようです。
https://julius.osdn.jp/juliusbook/ja/desc_adin.html

ノードのOSでdmesgなどでマイクが認識できている事を確認します。

```
# dmesg | grep usb
              :
[    3.867192] usb 1-1.4: Product: BUFFALO BSWHD06M USB Camera
[    3.874356] usb 1-1.4: Manufacturer: KYE Systems Corp.
               :  as /devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.4/1-1.4:1.0/input/input0
[    7.710343] usbcore: registered new interface driver uvcvideo
[    7.801623] usbcore: registered new interface driver snd-usb-audio
```

alsamixerで録音のレベル調整をしておきます。
[F6]のサウンドカード選択でデバイスの認識順が表示されるので、ここでしっかり認識順の数字を確認しておきます。
私の環境の場合、[F6]で「1 BUFFALO BSWHD06M USB Camera」を選択して、[F4]で調整する画面になりました。

![スクリーンショット 2021-11-26 10.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/cbec3d3d-17b9-5b5c-5870-f1a3f664a013.png)

## juliusコンテナイメージの作成
以下のDockerfileを作成し、マルチステージビルドでコンテナイメージを作成しました。
音声認識精度を上げるために、configureで「--enable-setup=standard」を付与しています。体感で気持ち程度の認識精度向上があります。

```:Dockerfile
FROM ubuntu:20.04 AS build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && \
    apt install --yes curl gcc libasound2-dev make unzip && \
    apt clean && \
    curl -fsSL https://github.com/julius-speech/julius/archive/refs/tags/v4.6.tar.gz | tar -xz && \
    mv /julius-4.6 /julius
WORKDIR /julius
RUN ./configure --build=aarch64 --prefix=/usr/local/lib/julius --with-mictype=alsa --enable-setup=standard && \
    make && \
    make install && \
    curl -fsSL "https://osdn.net/frs/redir.php?m=nchc&f=julius%2F71011%2Fdictation-kit-4.5.zip" -o dictation-kit.zip && \
    unzip dictation-kit.zip && \
    mv dictation-kit-* /usr/local/lib/julius/dictation-kit

FROM ubuntu:20.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && \
    apt install --yes libasound2 libopenmpi-dev && \
    apt clean
COPY --from=build /usr/local/lib/julius /usr/local
CMD ["julius","-C","/usr/local/dictation-kit/main.jconf","-C","/usr/local/dictation-kit/am-gmm.jconf","-nostrip","-input","mic","-module"]
```

Dockerfileを作成したディレクトリでビルドします。
個人的にdockerではなくpodmanを使用して、自宅内にレジストリサーバもあるので、コマンドとオプションは読み替えてください。

```
# podman build -t localhost:30500/julius:v4.6-std .
```

自宅にレジストリサーバがあるのでpushします。

```
# podman push --tls-verify=false localhost:30500/julius:v4.6-std
```

自宅にレジストリサーバがなければ、外部のレジストリサーバに登録するか、デプロイ予定のノードにイメージをコピーしてください。
デプロイ予定のノード上でコンテナイメージのビルドをするのもアリですが、高負荷で稼働中のPodのヘルスチェックなどが失敗してリカバリが大変になります。

## kubernetesでjuliusのpodをデプロイする
以下のようなPodのマニフェストファイルを作成し、デプロイします。
環境変数「ALSADEV」の「plughw:1,0」の数字がマイク接続時の認識順となるので、環境に合うよう書き換えてください。

```yaml:julius-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: julius
  labels:
    app: julius
spec:
  containers:
  - name: julius
    image: localhost:30500/julius:v4.6-std
    resources:
      limits:
        cpu: 1
        memory: 500Mi
    securityContext:
      privileged: true
#    command: ['sh','-c']
#    args:
#    - |
#      julius -C /usr/local/dictation-kit/main.jconf -C /usr/local/dictation-kit/am-dnn.jconf -dnnconf /usr/local/dictation-kit/julius.dnnconf -nostrip -input mic -module
    env:
    - name: ALSADEV
      value: "plughw:1,0"
    ports:
    - name: julius
      containerPort: 10500
  nodeName: maya
  hostNetwork: true
```

認識精度を更に求めるのであれば、コメントしている部分を外してDNN(Julius単体)版で起動してください。DNN(Julius単体)版ではCPU数とRAMの割り当ては少なくとも倍(CPU: 2,memory: 1Gi)に増やしてください。
認識までの時間は遅くなります。(体感で5秒〜)

個人的に、1台しかないノードをhostNetworkで扱うような特殊なPodは、コントローラ経由でPodを作成しない方が良いかと思います。他のノードで動かせないので、ノードのリソース枯渇時に無限増殖するような動作をした記憶があります。
デバイス、リソースに問題なければデプロイします。

```
# kubectl apply -f julius-pod.yaml 
pod/julius created
# watch kubectl get pods
Every 2.0s: kubectl get pods                                                                                                                         chino: Fri Nov 26 18:29:30 2021

NAME                                         READY   STATUS    RESTARTS        AGE
julius                                       1/1     Running   0               5m
```

無事「Running」になって、RESTARTSが増えていなければOKです。
うまく動作しない原因の大体がマイクの認識部分ですので、マイクを変えてdmesgを確認して対処してください。

## Node-REDで音声認識イベントを拾って処理する
標準ノードの「tcp-in」ノードを設定するだけで、音声認識のイベントを拾えます。
種類で「接続」を選択し、ポートを「10500」番、ホストにデプロイしたノードのホスト名(IPアドレスでも可)を指定します。
出力は「ストリーム」で「文字列」としてペイロードしました。めちゃくちゃ簡単ですね。

![スクリーンショット 2021-11-26 18.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/ce5699c1-7fa9-2670-3377-018888de221f.png)

次に、標準ノードの「switch」ノードを繋げて、正規表現で「<SHYPO」を含むデータだけ拾うようにしました。

![スクリーンショット 2021-11-26 18.46.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/e83d3585-5402-7a6d-1400-a75fdeeeb6ac.png)

更に、標準ノードの「html」ノードを繋げて、「WHYPO」タグとオプションを配列オブジェクトにします。ここらは行き当たりばったりで作ってます。

![スクリーンショット 2021-11-26 18.49.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/6c17d5e9-049f-7122-8f8e-1a6d9bd2799b.png)

最後に、標準ノードの「function」ノードを繋げて、配列オブジェクト内のwordオブジェクトを「msg.talk」に代入しながら繋げます。

![スクリーンショット 2021-11-26 18.57.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/c52f6647-09e1-4b4a-1570-2eb45998ed61.png)

「msg.talk」に認識した単語が入っているので、あとはswitchノードを通して煮るなり焼くなり調理しましょう。

![スクリーンショット 2021-11-26 19.02.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/54ef3a0e-5bb7-257f-54b3-f3b368aaa324.png)

動的な音声で返す「対話」をするのであれば、TTSサービスは欠かせませんので、以下の記事も参考にしてください。

https://qiita.com/Y-Shikase/items/2d773dc4d970228437d5

#おわりに
頑張って音声の認識精度を向上させ、できる処理を増やしていけば、クラウドサービスに依存しないスマートスピーカーのようなものができると思います。Alexaで音声を拾ってNode-REDへ連携させていますが、正直なところウェイクワードの「アレクサ！」を言いたくないんですよね。

3Dアバターと会話ができる入り口まで実装でき、更に愛着が湧いて「娘」になっています。

https://twitter.com/shikasey/status/1463808360453591040?s=21

Voiced by https://CoeFont.CLOUD
声は「[あみたろ・素直(小春音アミ)](https://coefont.cloud/coefonts/5af5520c-e976-43fe-9fa5-ed37ffd781f3)」を使用しています。