Text To Speech (Open Jtalk) を、Kubernetes Cronjob からホストのオーディオインターフェイスを使って喋らせる

TORICOでは、社内にツール用サーバが何台かあり、社内ツールはすべて Kubernetes + Docker で起動しています。

オフィスでは、決まった時間に Text To Speech (Open Jtalk)で、「換気しましょう」といったアナウンスをするようにしています。

以前は Ubuntu にそのまま Open JTalk をインストールし、生cron で実行していたのですが、最近そのサーバマシンを停止してすべて Kubernetes運用になりましたので、Text To Speech していたタスクを生 cron から Kubernetes Cronjob に移行しました。

# Dockerイメージを作る

## Dockerfile

まず、Open-JTalk がインストールされた Docker イメージを作ります。

apt install open-jtalk でもインストールできますが、バージョンが古いのでソースからビルドしたほうが使えるオプションが増えていて良いです。音量設定など。

mei ボイスを 1.7, 1.8 の2バージョン入れているのは、切り替えて使えるようにするためです。1.8では1.7からけっこう変化があり、、声のトーンが落ち着いてます。1.7の方がよく通る声なのでそっちを多く使っています。

マルチステージビルドしない場合、1GBほどのイメージサイズになります。この Dockerfile のようにマルチステージビルドした場合、130MBほどで済みました。Alpineは試していません。


```Dockerfile:Dockerfile
FROM ubuntu:18.04 as builder

WORKDIR /var/speech

RUN apt update && \
    apt install -y \
    unzip \
    curl \
    build-essential \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*

RUN curl -L -O https://downloads.sourceforge.net/hts-engine/hts_engine_API-1.10.tar.gz
RUN tar zxvf hts_engine_API-1.10.tar.gz
RUN cd hts_engine_API-1.10 && ./configure && make  && cd -

RUN curl -L -O https://downloads.sourceforge.net/open-jtalk/open_jtalk-1.11.tar.gz
RUN tar zxvf open_jtalk-1.11.tar.gz
RUN cd open_jtalk-1.11 && ./configure --with-charset=UTF-8 \
    --with-hts-engine-header-path=/var/speech/hts_engine_API-1.10/include \
    --with-hts-engine-library-path=/var/speech/hts_engine_API-1.10/lib \
    && make && make install

RUN mkdir /usr/share/hts-voice
RUN curl -L -O https://downloads.sourceforge.net/mmdagent/MMDAgent_Example-1.7.zip
RUN unzip MMDAgent_Example-1.7.zip

RUN curl -L -O https://downloads.sourceforge.net/mmdagent/MMDAgent_Example-1.8.zip
RUN unzip MMDAgent_Example-1.8.zip

FROM ubuntu:18.04

WORKDIR /var/speech

RUN apt update && \
    apt install -y \
    open-jtalk-mecab-naist-jdic \
    alsa-utils \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*


COPY --from=builder /usr/local/bin/open_jtalk /usr/local/bin/open_jtalk
COPY --from=builder /var/speech/MMDAgent_Example-1.7/Voice/mei /usr/share/hts-voice/mei17
COPY --from=builder /var/speech/MMDAgent_Example-1.8/Voice/mei /usr/share/hts-voice/mei18

COPY wav ./wav
COPY sh ./sh

```


## TTS再生スクリプト

sh ディレクトリの中身はこのようになっています。

```bash:sh/chime-speech.sh
#!/usr/bin/env bash

if [ ! "$1" ]; then
  echo "Usage: $0 <speech-text>" >2
  exit 1
fi

cd "$(dirname $0)" || exit

./ring-chime.sh
./jsay-female.sh "$1"
```

```bash:sh/jsay-female.sh
#!/bin/sh
TMP=/tmp/jsay.wav
echo "$1" | open_jtalk \
    -m /usr/share/hts-voice/mei/mei_normal.htsvoice \
    -x /var/lib/mecab/dic/open-jtalk/naist-jdic \
    -r 0.7 \
    -g 6 \
    -ow $TMP && \
./play-wav.sh $TMP
```

```bash:sh/play-wav.sh
#!/usr/bin/env bash
aplay -D plughw:1 ${1}
```

```bash:ring-chime.sh
#!/usr/bin/env bash
cd "$(dirname $0)" || exit
./play-wav.sh ../wav/chime.wav
```

`./chime-speech.sh こんにちは` というコマンドで、チャイム効果音を鳴らした後、「こんにちは」とTTSで発声するようになっています。

なお、`sh/play-wav.sh` の `plughw:1` の「1」はサウンドカード番号であり、Dockerホストの環境に依存すると思います。詳細は後ほど書きます。


## ビルドスクリプト
docker-compose でも良いのですが、私は下記のような bash スクリプトで Docker イメージのビルドとサーバへのデプロイをしています。

```bash:config.sh
#!/usr/bin/env bash
image_name=torico/torico-speech
container_name=torico-speech
deploy_host=user@deploy-server.example.com
```

```bash:build.sh
#!/usr/bin/env bash
cd "$(dirname $0)" || exit
. config.sh
docker build . -t ${image_name}
```

```bash:docker-image-copy-to-remote.sh
#!/usr/bin/env bash
. config.sh
docker save ${image_name} | bzip2 | ssh ${deploy_host} 'bunzip2 | sudo docker load'
```

↑手元でビルドした Docker イメージをリモートサーバにコピーします

```bash:login.sh
#!/usr/bin/env bash
./run.sh /bin/bash
```

```bash:run.sh
#!/usr/bin/env bash
cd "$(dirname $0)" || exit
. config.sh
docker run \
    --rm -it \
    --device /dev/snd \
    --name=${container_name} ${image_name} \
    "$@"
```

スクリプトファイルをたくさん作るのは、エディタ (IntelliJなど) で右クリックからの実行を簡単にするためです。

![Screenshot 2019-12-12 17.57.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68302/103627f5-a76b-1ad0-7572-a441577383b9.png)

build.sh でビルドしたら、docker-image-copy-to-remote.sh で SSH でサーバに送りつけます。今回は Docker リポジトリは使っていません。

./run.sh は動作確認のために作っています。Ubuntu用です。Mac では /dev/snd が無いので動きません。

# サウンドインターフェイスの確認

`--device /dev/snd` をつけて docker コンテナを起動し、サウンドカードの様子を見ます。 `--privileged` はなくても動きました

```bash
sudo docker run --rm -it --device /dev/snd --name=torico-speech torico/torico-speech /bin/bash
```

```bash
# alsamixer
```
![Screenshot 2019-12-12 17.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68302/9a98d05e-728d-1fc8-973d-689ede449cb8.png)


```bash
root@f3a4dc37f277:/var/speech# amixer
Simple mixer control 'IEC958',0
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'IEC958',1
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'IEC958',2
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'IEC958',3
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'IEC958',4
  Capabilities: pswitch pswitch-joined
  Playback channels: Mono
  Mono: Playback [on]

root@f3a4dc37f277:/var/speech# amixer -c 1
Simple mixer control 'Master',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback 0 - 87
  Mono: Playback 87 [100%] [0.00dB] [on]
Simple mixer control 'Headphone',0
  Capabilities: pvolume pswitch
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 87
  Mono:
  Front Left: Playback 87 [100%] [0.00dB] [on]
  Front Right: Playback 87 [100%] [0.00dB] [on]
Simple mixer control 'Speaker',0
  Capabilities: pswitch
  Playback channels: Front Left - Front Right
  Mono:
  Front Left: Playback [on]
  Front Right: Playback [on]
Simple mixer control 'Speaker+LO',0
  Capabilities: pvolume
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 87
  Mono:
  Front Left: Playback 87 [100%] [0.00dB]
  Front Right: Playback 87 [100%] [0.00dB]
Simple mixer control 'Front Mic',0
  Capabilities: pvolume pswitch
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 31
  Mono:
  Front Left: Playback 0 [0%] [-34.50dB] [off]
  Front Right: Playback 0 [0%] [-34.50dB] [off]
...
root@f3a4dc37f277:/var/speech# aplay wav/chime.wav
ALSA lib pcm_dmix.c:1052:(snd_pcm_dmix_open) unable to open slave
aplay: main:788: audio open error: No such file or directory

root@f3a4dc37f277:/var/speech# aplay -D plughw:1 wav/chime.wav
Playing WAVE 'wav/chime.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
(再生される)
root@f3a4dc37f277:/var/speech#
```

サウンドカードは2つ認識されていて、使いたいカード番号は 1 でした。
なので、aplay の引数に `-D plughw:1` をつけます。




# Kubernetes cronjob

ローカルにある Docker イメージを使って Cronjob を起動します。

ローカルの Docker イメージを使うので、imagePullPolicy は Never です。

```yaml:cronjob.yml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: speech-12-open
  namespace: torico
  labels:
    cronjob: speech-12-open
spec:
  concurrencyPolicy: Replace
  schedule: "0 12 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: torico-speech
              image: torico/torico-speech
              imagePullPolicy: Never
              args:
                - "/var/speech/sh/chime-speech.sh"
                - "12時になりました。…窓を開けて、5分間換気をしてください。"
              volumeMounts:
              - mountPath: /dev/snd
                name: dev-snd
              securityContext:
                privileged: true
          volumes:
            - name: dev-snd
              hostPath:
                path: /dev/snd
```

docker run の `--device` オプションの代わりに、volumeMounts を使います。privileged が必要です。

ちなみに私は、上記ファイルと同じディレクトリに apply.sh とか作って、エディタから右クリックでkubectl を実行するようにしていまします。

```bash:apply.sh
#!/usr/bin/env bash

export KUBECONFIG=${HOME}/.kube/remote-server-kubeconfig

kubectl apply -f cronjob.yml
```

![Screenshot 2019-12-12 19.30.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68302/41e7e4ac-8065-5612-d42a-e553e96d4fdc.png)