作業中のメモ

よく「計算機」を使って作業をする.知らなかったことを中心にまとめるつもり.

Raspberry Pi 3 Model BでNginx+PostgreSQL+Django環境構築

どうも,筆者です.

前回,Docker のインストールを行った. workspacememory.hatenablog.com

今回は,Dockerfile と docker-compose.yml を使って Nginx+PostgreSQLDjango 環境を構築する. 参考1 参考2

構成

.
│  docker-compose.yml
│
├─django
│      Dockerfile
│      requirements.txt
│      uwsgi.ini
│
├─logs
│      access.log
│      error.log
│      uwsgi.log
│
├─nginx
│  │  Dockerfile
│  │
│  ├─cache
│  ├─conf
│  │      default.conf
│  │      nginx.conf
│  │      uwsgi_params
│  │
│  └─dockerApp
│          nginx
│          wakeup.sh
│
├─postgresql
│      Dockerfile
│
├─src
│  │  manage.py
│  │
│  └─membership_system
│      │  settings.py
│      │  urls.py
│      │  wsgi.py
│      │  __init__.py
│      │
│      └─__pycache__
│              settings.cpython-36.pyc
│              urls.cpython-36.pyc
│              __init__.cpython-36.pyc
│
└─static
    └─admin
        ├─css
        │  │  autocomplete.css
        │  │  base.css
        │  │  changelists.css
        │  │  dashboard.css
        │  │  fonts.css
        │  │  forms.css
        │  │  login.css
        │  │  responsive.css
        │  │  responsive_rtl.css
        │  │  rtl.css
        │  │  widgets.css
        │  │
        │  └─vendor
        │      └─select2
        │              LICENSE-SELECT2.md
        │              select2.css
        │              select2.min.css
        │
        ├─fonts
        │      LICENSE.txt
        │      README.txt
        │      Roboto-Bold-webfont.woff
        │      Roboto-Light-webfont.woff
        │      Roboto-Regular-webfont.woff
        │
        ├─img
        │  │  calendar-icons.svg
        │  │  icon-addlink.svg
        │  │  icon-alert.svg
        │  │  icon-calendar.svg
        │  │  icon-changelink.svg
        │  │  icon-clock.svg
        │  │  icon-deletelink.svg
        │  │  icon-no.svg
        │  │  icon-unknown-alt.svg
        │  │  icon-unknown.svg
        │  │  icon-yes.svg
        │  │  inline-delete.svg
        │  │  LICENSE
        │  │  README.txt
        │  │  search.svg
        │  │  selector-icons.svg
        │  │  sorting-icons.svg
        │  │  tooltag-add.svg
        │  │  tooltag-arrowright.svg
        │  │
        │  └─gis
        │          move_vertex_off.svg
        │          move_vertex_on.svg
        │
        └─js
            │  actions.js
            │  actions.min.js
            │  autocomplete.js
            │  calendar.js
            │  cancel.js
            │  change_form.js
            │  collapse.js
            │  collapse.min.js
            │  core.js
            │  inlines.js
            │  inlines.min.js
            │  jquery.init.js
            │  popup_response.js
            │  prepopulate.js
            │  prepopulate.min.js
            │  prepopulate_init.js
            │  SelectBox.js
            │  SelectFilter2.js
            │  timeparse.js
            │  urlify.js
            │
            ├─admin
            │      DateTimeShortcuts.js
            │      RelatedObjectLookups.js
            │
            └─vendor
                ├─jquery
                │      jquery.js
                │      jquery.min.js
                │      LICENSE-JQUERY.txt
                │
                ├─select2
                │  │  LICENSE-SELECT2.md
                │  │  select2.full.js
                │  │  select2.full.min.js
                │  │
                │  └─i18n
                │          ar.js
                │          az.js
                │          bg.js
                │          ca.js
                │          cs.js
                │          da.js
                │          de.js
                │          el.js
                │          en.js
                │          es.js
                │          et.js
                │          eu.js
                │          fa.js
                │          fi.js
                │          fr.js
                │          gl.js
                │          he.js
                │          hi.js
                │          hr.js
                │          hu.js
                │          id.js
                │          is.js
                │          it.js
                │          ja.js
                │          km.js
                │          ko.js
                │          lt.js
                │          lv.js
                │          mk.js
                │          ms.js
                │          nb.js
                │          nl.js
                │          pl.js
                │          pt-BR.js
                │          pt.js
                │          ro.js
                │          ru.js
                │          sk.js
                │          sr-Cyrl.js
                │          sr.js
                │          sv.js
                │          th.js
                │          tr.js
                │          uk.js
                │          vi.js
                │          zh-CN.js
                │          zh-TW.js
                │
                └─xregexp
                        LICENSE-XREGEXP.txt
                        xregexp.js
                        xregexp.min.js

docker-compose.yml

docker-compose コマンドを実行する際に利用するdocker-compose.ymlファイルを以下に示す.

version: '3.4'

volumes:
    # DBデータの永続化
    postgresql.volume:
        name: postgresql.volume

services:
    # nginxの設定
    nginx:
        # ビルド対象
        build:
            context: ./nginx
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: custom_nginx
        # コンテナ名
        container_name: nginx
        restart: always
        # 環境変数の設定
        environment:
            TZ: Asia/Tokyo
        # ポートの設定(DockerfileのEXPOSEとそろえる)
        ports:
            - "18082:18082"
        # nginxのconfigファイル等の関連付け
        volumes:
            - ./nginx/conf/default.conf:/etc/nginx/conf.d/default.conf:ro
            - ./nginx/cache:/var/cache/nginx
            - ./logs/access.log:/var/log/nginx/access.log
            - ./logs/error.log:/var/log/nginx/error.log
            - ./static:/static:ro
        # 依存関係の記述
        depends_on:
            - django

    # DBの設定
    db:
        # ビルド対象
        build:
            context: ./postgresql
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: custom_postgres
        # コンテナ名
        container_name: postgresql
        restart: always
        environment:
            POSTGRES_DB: djangodb
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: 1234
            POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
        # ポートの設定(外部には公開しない)
        expose:
            - "5432"
        # 設定ファイルとデータ格納先の関連付け
        volumes:
            - postgresql.volume:/var/lib/postgresql/data

    # djangoの設定
    django:
        # ビルド対象
        build:
            context: ./django
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: django_with_uwsgi
        # コンテナ名
        container_name: django
        restart: always
        # 利用するアプリの関連付け
        volumes:
            - ./src:/code
            - ./static:/static:ro
            - ./logs/uwsgi.log:/var/log/uwsgi.log
        working_dir: /code
        # ポートの設定(外部には公開しない)
        expose:
            - "8081"
        # 依存関係の記述
        depends_on:
            - db

Django の設定

ここでは,Django の設定内容について説明する.

Dockerfile

Django のイメージファイルを作成する際に利用する Dockerfile を以下に示す.

FROM resin/raspberry-pi-alpine:3.6
# Pythonで利用する環境変数の設定
# バイナリレイヤ下での標準出力とエラー出力を抑制
ENV PYTHONUNBUFFERED 1
# アプリケーション用のディレクトリの作成
RUN mkdir /code && mkdir /static
# requirements.txtを追加
ADD requirements.txt /code/

# パッケージ群のインストール
RUN    apk update \
    # タイムゾーンの設定
    && apk add --no-cache bash tzdata pcre-dev \
    && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
    # PostgreSQL用のライブラリをインストール
    && apk add --no-cache postgresql-dev \
    # 一時的に必要なパッケージをインストール
    && apk add --no-cache \
           --virtual .build-temp shadow \
           gcc libc-dev linux-headers \
           musl-dev python3-dev libffi-dev libressl-dev \
    # Python3をインストール
    && apk add --no-cache python3 \
    # pip install
    && python3 -m ensurepip \
    && rm -r /usr/lib/python*/ensurepip \
    && pip3 install --upgrade pip setuptools \
    # requirements.txtに記載されたライブラリ群をインストール
    && pip3 install -r /code/requirements.txt \
    # シンボリックリンク
    && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi \
    # ユーザとグループの追加(Nginxと連動するため統一)
    && useradd -s /sbin/nologin -M -d /dev/null nginx \
    # 一時的に必要だったパッケージを削除
    && apk del .build-temp \
    && rm -rf /root/.cache /var/cache/apk/*

# uWSGIの設定ファイルをコピー
COPY ./uwsgi.ini /uwsgi.ini
# ログ出力先の設定
RUN echo -n > /var/log/uwsgi.log

# 内部で利用するポートの設定
EXPOSE 8081

# コマンド実行
CMD ["uwsgi", "--ini", "/uwsgi.ini", "--logto", "/var/log/uwsgi.log"]

Django で利用する設定ファイル

Django の環境構築時に利用する設定ファイルを以下に示す.

requirements.txt

Django==2.0.7
uwsgi==2.0.17
psycopg2==2.8.2
psycopg2-binary==2.8.2
hashids==1.2.0
django-betterforms==1.2
django-bootstrap4==0.0.6
django-crispy-forms==1.7.2
django-filter==2.1.0
django-extra-views==0.11.0
django-widgets-improved==1.5.0

uwsgi.ini

[uwsgi]
user           = nginx
uid            = nginx
gid            = nginx
chdir          = /code
module         = membership_system.wsgi:application
master         = true
enable-threads = true
thunder-lock   = true
max-requests   = 1000
processes      = 1
threads        = 2
vacuum         = true
socket         = :8081
close-on-exec  = true
die-on-term    = true

docker-compose の設定(対応箇所の抜粋)

対応する docker-compose の設定情報を以下に示す.今回は,8081 番のポートで待ち受ける.このポートは外部に公開しない.

    # djangoの設定
    django:
        # ビルド対象
        build:
            context: ./django
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: django_with_uwsgi
        # コンテナ名
        container_name: django
        restart: always
        # 利用するアプリの関連付け
        volumes:
            - ./src:/code
            - ./static:/static:ro
            - ./logs/uwsgi.log:/var/log/uwsgi.log
        working_dir: /code
        # ポートの設定(外部には公開しない)
        expose:
            - "8081"
        # 依存関係の記述
        depends_on:
            - db

PostgreSQL の設定

ここでは,PostgreSQL の設定内容について説明する. 参考

Dockerfile

Django のイメージファイルを作成する際に利用する Dockerfile を以下に示す.

FROM postgres:latest
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.UTF-8

docker-compose の設定(対応箇所の抜粋)

対応する docker-compose の設定情報を以下に示す.今回は,5432 番のポートで待ち受ける.このポートは外部に公開しない.

volumes:
    # DBデータの永続化
    postgresql.volume:
        name: postgresql.volume
    # DBの設定
    db:
        # ビルド対象
        build:
            context: ./postgresql
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: custom_postgres
        # コンテナ名
        container_name: postgresql
        restart: always
        environment:
            POSTGRES_DB: djangodb
            POSTGRES_USER: postgres
            POSTGRES_PASSWORD: 1234
            POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
        # ポートの設定(外部には公開しない)
        expose:
            - "5432"
        # 設定ファイルとデータ格納先の関連付け
        volumes:
            - postgresql.volume:/var/lib/postgresql/data

また,ユーザ名とパスワードは以下のようにした.そのまま利用する際は,適宜変更すること.

項目 内容
ユーザ名 postgres
パスワード 1234

Nginx の設定

ここでは,Nginx の設定内容について説明する.

Dockerfile

Nginx のイメージファイルを作成する際に利用する Dockerfile を以下に示す.

FROM resin/raspberry-pi-alpine:3.6

# Nginxのバージョン指定
ENV NGINX_VERSION 1.14.0
# Nginxのインストール
RUN    apk update \
    && apk add --no-cache pcre-dev openssl-dev bash shadow tzdata \
    && useradd -s /sbin/nologin -M -d /dev/null nginx \
    && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
    && apk add --no-cache --virtual build-dependencies build-base curl \
    && curl -SLO http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
    && tar xzvf nginx-${NGINX_VERSION}.tar.gz \
    && cd nginx-${NGINX_VERSION} \
    && ./configure \
           --user=nginx \
           --group=nginx \
           --with-http_ssl_module \
           --with-http_realip_module \
           --with-http_addition_module \
           --with-http_sub_module \
           --with-http_dav_module \
           --with-http_flv_module \
           --with-http_mp4_module \
           --with-http_gunzip_module \
           --with-http_gzip_static_module \
           --with-http_random_index_module \
           --with-http_secure_link_module \
           --with-http_stub_status_module \
           --with-http_auth_request_module \
           --with-threads \
           --with-stream \
           --with-stream_ssl_module \
           --with-http_slice_module \
           --with-mail \
           --with-mail_ssl_module \
           --with-file-aio \
           --with-http_v2_module \
           --prefix=/usr/share/nginx \
           --sbin-path=/usr/local/sbin/nginx \
           --conf-path=/etc/nginx/nginx.conf \
           --pid-path=/var/run/nginx.pid \
           --http-log-path=/var/log/nginx/access.log \
           --error-log-path=/var/log/nginx/error.log \
    && make \
    && make install \
    && echo -n > /var/log/nginx/access.log \
    && echo -n > /var/log/nginx/error.log \
    && cd / \
    && apk del build-dependencies shadow \
    && rm -rf nginx-${NGINX_VERSION} nginx-${NGINX_VERSION}.tar.gz /var/cache/apk/*

# static directory for django
RUN mkdir /static

# volumeの設定
VOLUME /var/cache/nginx

# 設定の書き換え
COPY ./conf/nginx.conf /etc/nginx/nginx.conf
COPY ./conf/uwsgi_params /etc/nginx/uwsgi_params

# 内部で利用するポートの設定
EXPOSE 18082

# 利用するシェルスクリプトをコンテナに配置
COPY ./dockerApp/wakeup.sh /wakeup.sh
COPY ./dockerApp/nginx /etc/init.d/nginx
RUN chmod a+x /wakeup.sh /etc/init.d/nginx

# entrypoint: 何もしない
ENTRYPOINT [""]
# コマンド実行
CMD ["/wakeup.sh"]

また,利用する shell script を以下に示す.

nginx

#!/bin/bash
set -e
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="nginx daemon"
NAME=nginx
DAEMON=/usr/local/sbin/${NAME}
SCRIPTNAME=/etc/init.d/${NAME}

# if daemon file not found, exit script.
test -x ${DAEMON} || exit 0

d_start() {
    ${DAEMON} || echo -n " already running"
}

d_stop() {
    ${DAEMON} -s quit || echo -n " not running"
}

d_reload() {
    ${DAEMON} -s reload || echo -n " could not reload"
}

case "$1" in
    start)
        echo -n "Starting ${DESC}: ${NAME}"
        d_start
        sleep 3
        echo "."
        ;;

    stop)
        echo -n "Stopping ${DESC}: ${NAME}"
        d_stop
        sleep 3
        echo "."
        ;;

    reload)
        echo -n "Reloading ${DESC}: configuration..."
        d_reload
        sleep 3
        echo "reloaded."
        ;;

    restart)
        echo -n "Restarting ${DESC}: ${NAME}"
        d_stop
        sleep 5
        d_start
        echo "."
        ;;
    *)
        echo "Usage: ${SCRIPTNAME} {start|stop|restart|reload}" >&2
        exit 3
        ;;
esac

exit 0

wakeup.sh

#!/bin/bash

# Nginx start
/etc/init.d/nginx start

# hook SIGTERM
trap_TERM() {
        echo SIGTERM ACCEPTED.
        exit 0
}
trap 'trap_TERM' TERM

# loop
while :
do
        sleep 5
done

uWSGI の設定ファイル

Nginx で受けたリクエストを Django に投げる際の uWSGI の設定ファイルを以下に示す.

uwsgi_params

uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;

Nginx の設定ファイル

Nginx のコンフィグファイルを以下に示す.

nginx.conf

# user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    server_names_hash_bucket_size 64;
    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

default.conf

# the upstream component nginx needs to connect to
upstream call_django {
        ip_hash;
        server django:8081;
}

# configuration of the server
server {
        listen 18082;
        server_name _;
        server_tokens off;
        charset     utf-8;
        access_log  /var/log/nginx/access.log combined;
        error_log   /var/log/nginx/error.log error;

        # max upload size
        client_max_body_size 64M; # adjust to taste

        location /static {
                alias /static;
        }

        # Finally, send all non-media requests to the Django server.
        location / {
                # avoid requests that are neither GET nor POST
                if ( $request_method !~ ^(GET|POST)$ ) {
                        return 444;
                }

                # setting
                include          /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
                uwsgi_pass       call_django;
        }
}

docker-compose の設定(対応箇所の抜粋)

対応する docker-compose の設定情報を以下に示す.今回は,18082 番のポートで待ち受ける.

    nginx:
        # ビルド対象
        build:
            context: ./nginx
            # Dockerfile名
            dockerfile: Dockerfile
        # イメージ名
        image: custom_nginx
        # コンテナ名
        container_name: nginx
        restart: always
        # 環境変数の設定
        environment:
            TZ: Asia/Tokyo
        # ポートの設定(DockerfileのEXPOSEとそろえる)
        ports:
            - "18082:18082"
        # nginxのconfigファイル等の関連付け
        volumes:
            - ./nginx/conf/default.conf:/etc/nginx/conf.d/default.conf:ro
            - ./nginx/cache:/var/cache/nginx
            - ./logs/access.log:/var/log/nginx/access.log
            - ./logs/error.log:/var/log/nginx/error.log
            - ./static:/static:ro
        # 依存関係の記述
        depends_on:
            - django

Docker Image 作成

Docker Image 作成のため,下記を実行する.

docker-compose build

log ファイルの作成

実行ログを記録するための log ファイルを作成する.

touch logs/uwsgi.log logs/access.log logs/error.log

Django のプロジェクトを作成

Django のプロジェクトを作成するため,下記のコマンドを実行する. 参考

# コンテナの作成
docker-compose up -d
# django のコンテナに入る
docker exec -it django bash
# プロジェクトの作成
django-admin startproject membership_system .
# コンテナから抜ける
exit
# 所有者の変更
sudo chown pi:pi -R src/

Django の設定

setting.py の編集

setting.py を修正する.

修正前

ALLOWED_HOSTS = []

# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

修正後

ALLOWED_HOSTS = ['localhost']

# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

# 修正
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'djangodb',
        'USER': 'postgres',
        'PASSWORD': '1234',
        'HOST': 'db',
        'PORT': 5432,
    }
}

マイグレーションの実行

下記のコマンドを実行する.

docker exec -it django bash
# コンテナ内で作業
./manage.py makemigrations
./manage.py migrate
./manage.py createsuperuser

# いい感じに設定する
#Username (leave blank to use 'root'): admin
#Email address: admin@localhost
#Password:
#Password (again):

# コンテナから抜ける
exit

ページに接続する

ブラウザを開いて下記を入力する.「The install worked successfully! Congratulations!」が表示されれば成功.

http://(ホスト名orIPアドレス):18082

その他設定

タイムゾーンや言語,静的ファイルの設定を行う.

変更前

# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'en_US'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'

変更後

# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'ja-JP'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'
STATIC_ROOT = '/static'
# static フォルダの読み込み専用を取り外す
        volumes:
            - ./src:/code
#            - ./static:/static:ro
            - ./static:/static
            - ./logs/uwsgi.log:/var/log/uwsgi.log
        working_dir: /code
# コンテナの作り直し
docker rm -f $(docker ps -aq)
# コンテナの作成
docker-compose up -d
docker exec -it django bash
# コンテナ内で作業
./manage.py collectstatic
# コンテナから抜ける
exit

Raspberry Pi 3 Model BにDocker導入

どうも,筆者です.

久しぶりの更新となる.

目的

Raspberry Pi 3 Model B に Docker を導入し,Nginx,MySQLdjango を用いて,名簿作成システムを構築する.

実施内容

今回は,Raspberry Pi 3 Model B で下記の設定を行う.

  1. Docker のインストール
  2. Docker-Compose のインストール

Dockerのインストール

Raspberry Pi 3 Model B の環境を以下に示す.

cat /etc/os-release
#PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
#NAME="Raspbian GNU/Linux"
#VERSION_ID="8"
#VERSION="8 (jessie)"
#ID=raspbian
#ID_LIKE=debian
#HOME_URL="http://www.raspbian.org/"
#SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
#BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

下記のコマンドで Docker をインストールする. 参考サイト

# Docker のインストール
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
echo "deb [arch=armhf] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y docker-ce

# バージョンチェック(実行にはroot権限が必要)
sudo docker version
# 出力例
#Client:
# Version:           18.06.3-ce
# API version:       1.38
# Go version:        go1.10.3
# Git commit:        d7080c1
# Built:             Wed Feb 20 02:48:01 2019
# OS/Arch:           linux/arm
# Experimental:      false
#
#Server:
# Engine:
#  Version:          18.06.3-ce
#  API version:      1.38 (minimum version 1.12)
#  Go version:       go1.10.3
#  Git commit:       d7080c1
#  Built:            Wed Feb 20 02:43:39 2019
#  OS/Arch:          linux/arm
#  Experimental:     false

# ユーザを docker グループに追加(root権限が不要になる)
sudo usermod -aG docker ${USER}

# 一度ログアウトし,再度ログイン
exit
### ログイン処理 ###

# 再度,バージョンを確認
docker version

Docker-Compose のインストール

下記のコマンドで,Docker-Compose のインストールを実施する. 参考サイト

# git のインストール
sudo apt-get install -y git

# compose を clone
git clone https://github.com/docker/compose.git

# compose のインストール
cd compose
git checkout bump-1.23.2 # ブランチの変更
sed -i -e 's:^VENV=/code/.tox/py36:VENV=/code/.venv; python3 -m venv $VENV:' script/build/linux-entrypoint
sed -i -e '/requirements-build.txt/ i $VENV/bin/pip install -q -r requirements.txt' script/build/linux-entrypoint
docker build -t docker-compose:armhf -f Dockerfile.armhf .
# しばらく待つ
docker run --rm --entrypoint="script/build/linux-entrypoint" -v $(pwd)/dist:/code/dist -v $(pwd)/.git:/code/.git "docker-compose:armhf"
sudo cp dist/docker-compose-Linux-armv7l /usr/local/bin/docker-compose
sudo chown root:root /usr/local/bin/docker-compose
sudo chmod 0755 /usr/local/bin/docker-compose
docker-compose version
#docker-compose version 1.23.2, build 1110ad01
#docker-py version: 3.6.0
#CPython version: 3.6.8
#OpenSSL version: OpenSSL 1.1.0j  20 Nov 2018

# イメージの削除
docker rmi $(docker images | grep docker-compose | awk '{print $3;}')

これで,Docker と Docker-Compose が導入できた.次は,Nginx,PostgreSQLdjango のイメージの構築を行う.

次回

workspacememory.hatenablog.com

音声認識による赤外線機器の操作 その 7【全体制御編】

どうも,筆者です.

前回までで,認識した単語の解析まで行えるようになった.後は,選択した単語に対応する応答メッセージ(音声)を流しつつ,赤外線信号を送信すればよい.

前回までの記事は以下にある.

workspacememory.hatenablog.com

今回の対象

今回の実装対象を以下に示す.

今回の実装対象

実装対象一覧

実装するクラスを以下に示す.

  • audioPlayer.py(応答メッセージ再生用)
  • getWeather.py(天気予報取得用)
  • voiceRecognition.py(音声認識用,メイン部分)

これらを順に実装する.

audioPlayer

音声再生には,Linux の aplay コマンドを利用する.「audioPlayer.py」の実装を以下に示す.

~/juliusKit $ touch audioPlayer.py
~/juliusKit $ vim audioPlayer.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# audioPlayer.py

import subprocess as sb
from VR_ConstClass import CONST_CLASS

class audioClass():
    def __init__(self):
        self.__audioFile = ''
        self.__process = sb.Popen(['/bin/echo', '0.001'], shell=True, stdout=sb.DEVNULL, stderr=sb.DEVNULL)
        self.__process.communicate()

    def setAudioFile(self, audio_file):
        self.__audioFile = audio_file

    def audioExitHandler(self):
        if self.__process.poll() is None:
            self.__process.terminate()

    def playAudio(self):
        if self.__audioFile == '':
            return
        self.audioExitHandler()
        useCmd = ['/usr/bin/aplay', CONST_CLASS.WAVE_DIR + self.__audioFile]
        self.__process = sb.Popen(useCmd, stdout=sb.DEVNULL, stderr=sb.DEVNULL)

getWeather

天気予報の取得には,Weather Hacks API を利用する.使い方は,以下のサイトを参考にした.

tarao-mendo.blogspot.com

また,受信した情報を読み上げたいと考えたため,「Open JTalk」を利用して音声再生を行った.ここだけ,天気予報受信→再生という流れを取っているため,非同期で処理できていない.インストール方法は以下を参考にした.

www.taneyats.com

インストール後,実装したコードを以下に示す.ファイル名を「getWeather.py」として保存した.

~/juliusKit $ touch getWeather.py
~/juliusKit $ vim getWeather.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# getWeather.py

import requests
import subprocess as sb
from VR_ConstClass import CONST_CLASS

class weatherClass():
    def __init__(self):
        self.__url = 'http://weather.livedoor.com/forecast/webservice/json/v1'
        self.__city = 130010
        self.__titleStr = 'title'
        self.__loopStr = 'forecasts'
        self.__dateStr = 'date'
        self.__telopStr = 'telop'
        self.__dicDir = '/usr/local/share/openJTalk/dic/'
        self.__voiceFile = '/usr/local/share/openJTalk/voice/mei_normal.htsvoice'
        self.__process = sb.Popen(['/bin/echo', '0.001'], shell=True, stdout=sb.DEVNULL, stderr=sb.DEVNULL)
        self.__process.communicate()

    def __getWeather(self):
        getURL = '{0}?city={1}'.format(self.__url, self.__city)
        apiData = requests.get(getURL).json()
        retList = [apiData[self.__titleStr]]

        for weather in apiData[self.__loopStr]:
            tmpData = list(map(lambda x: int(x, 10), weather[self.__dateStr].split('-')))
            weatherDate = '{0}年{1}月{2}日'.format(tmpData[0], tmpData[1], tmpData[2])
            weatherForecasts = weather[self.__telopStr]
            retList.append('{0},{1}'.format(weatherDate, weatherForecasts))
        return retList

    # 天気予報取得関数の終了処理
    def weatherExitHandler(self):
        if self.__process.poll() is None:
            self.__process.terminate()

    def run(self, wavFile='weather.wav'):
        listData = self.__getWeather()
        # 読み上げるテキスト
        text = ','.join(listData).replace(' ', '')
        output = CONST_CLASS.WAVE_DIR + wavFile
        outputList = ['/bin/echo', '"' + str(text) + '"', '|']
        outputList.extend(['/usr/local/bin/open_jtalk', '-m', self.__voiceFile, '-ow', output, '-x', self.__dicDir])
        # 実行コマンド
        command = ' '.join(outputList)
        try:
            # コマンドの実行
            self.__process = sb.Popen(command, shell=True, stdin=sb.PIPE, stdout=sb.PIPE, stderr=sb.PIPE)
            # 処理の完了待ち
            self.__process.communicate()
        except:
            self.weatherExitHandler()
        return text

voiceRecognition

メイン処理を実装している部分である.以前導入した「SimpleWebSocketServer」をメインで動作させる.メッセージを受信したら,対象の関数を呼び出し単語解析を実施する.解析結果から赤外線データの送信等の処理を行う.

以下のスクリプトを「voiceRecognition.py」として保存する.

~/juliusKit $ touch voiceRecognition.py
~/juliusKit $ vim voiceRecognition.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# voiceRecognition.py

from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
import signal
import parseJuliusData, adrsirlib, getWeather
import configuration, audioPlayer
from VR_ConstClass import CONST_CLASS

class voiceRecognitionClass():
    def __init__(self):
        # julius クラスのインスタンス
        self.__julius = parseJuliusData.parseJuliusDataClass()
        # configuration クラスのインスタンス
        self.__configure = configuration.voiceConfigClass()
        # voice の設定
        self.__audio = audioPlayer.audioClass()
        # weather の設定
        self.__weather = getWeather.weatherClass()

    # 処理開始
    def startExecution(self):
        self.__julius.startThread()

    # 処理終了
    def stopExecution(self):
        self.__julius.stopThread()          # julius の停止
        self.__audio.audioExitHandler()     # 音声の停止
        self.__weather.weatherExitHandler() # 天気予報のデータ生成停止

    # 実行用関数
    def analysis(self, wordData):
        # ToDo: スリープモード移行時
        if False:
            retVal = self.__configure.setJuliusState(False)
            # 動作中だった場合
            if retVal:
                # 停止状態に移行
                pass
        # ToDo: マニュアル操作時
        elif False:
            local_text = wordData
            sendCmd = self.__configure.getIrCmd(local_text)

            if sendCmd is not None:
                # ADRSIR にコマンドを送信
                adrsirlib.write(sendCmd)
        # それ以外
        else:
            retStatus, sendCmd, audioFile = self.__configure.chkCmdExection(wordData)
            retVal = (retStatus != self.__configure.juliusReturnState[False])
            self.__audio.setAudioFile(audioFile)

            if retVal:
                outputText = self.__printMsg[retVal]
                # 天気を読み上げる場合
                if retStatus == self.__configure.juliusReturnState['weather']:
                    outputText = self.__weather.run(wavFile=audioFile)
                self.__audio.playAudio()

                if sendCmd is not None:
                    # ADRSIR にコマンドを送信
                    adrsirlib.write(sendCmd)

class processStatusClass():
    def __init__(self):
        self.__running = True
        self.__stopped = False
        self.__status = self.__running
        signal.signal(signal.SIGINT, self.changeState)  # Ctrl + C の監視
        signal.signal(signal.SIGTERM, self.changeState) # kill コマンドの監視

    def changeState(self, signum, frame):
        self.__status = self.__stopped

    def getStatus(self):
        return self.__status

if __name__ == '__main__':
    # processStatus クラスのインスタンス
    procStat = processStatusClass()
    # voiceRecognitiond クラスのインスタンス
    vrc = voiceRecognitionClass()
    # 処理開始
    vrc.startExecution()
    # SimpleWebSocketServer 用のクラス定義
    class webSocketProcessClass(WebSocket):
        def handleMessage(self):
            vrc.analysis(self.data)
    # WebSocket 用のサーバのインスタンス生成
    server = SimpleWebSocketServer(
        CONST_CLASS.WEBSOCKET_HOST,
        CONST_CLASS.WEBSOCKET_PORT,
        webSocketProcessClass
    )

    # main loop
    while procStat.getStatus():
        server.serveonce()

    # 処理終了
    server.close()
    vrc.stopExecution()

ここまでのディレクトリ構成を以下に示す.

~/juliusKit
   |--dictationKit_v4.3.1
       |--word.dic
       |--word.jconf
   |--grammarKit
   |   |--controller
   |        |--compile.sh
   |        |--mkdfa.pl
   |        |--mkfa
   |        |--utf8_controller.grammar
   |        |--utf8_controller.voca
   |--outYomi.sh
   |--word.yomi
   |--VR_ConstClass.py
   |--parseJuliusData.py
   |--webSocketClient.py
   |--configuration.py
   |--adrsirlib.py         # 追加部分
   |--audioPlayer.py       # 追加部分
   |--getWeather.py        # 追加部分
   |--voiceRecognitiond.py # 追加部分
   |--jsonData
   |   |--TVData.json
   |   |--confParam.json
   |   |--lightData.json
   |--wavFile
        |--TVOff.wav
        |--TVOn.wav
        |--lightDown.wav
        |--lightOff.wav
        |--lightOn.wav
        |--lightUp.wav
        |--nightLight.wa

現状,Ctrl + C または,kill コマンドによりプログラムが終了するようになっている.メイン処理の「webSocketProcessClass」というクラスの定義方法がこれで正しいかどうかは分からないが,現状では,この方法でしか WebSocket のサーバを立てることができなかった.他によい方法があればそちらを採用したい.

関数の呼び出し方法等が分かりづらいため,何かしら資料を作成し可視化しておきたい.その前に,この Python スクリプトをデーモン化して,起動時に自動実行するような設定をしようと思っている.

余談

音声認識に記述文法を用いているため,誤認識が時々ある.この認識精度を上げるよい方法はないのだろうか.信頼度とかを使うべきか?

音声認識による赤外線機器の操作 その 6【単語解析編】

どうも,筆者です.

前回は,以下の 3 つを実装した.

  • Julius の起動
  • Julius からの認識結果の取得
  • WebSocket を用いて認識結果を送信

前回までの記事は以下にある.

workspacememory.hatenablog.com

製作状況

製作状況を以下に示す.パーサーの部分は製作が完了している.今回は,単語解析の部分を実装する.

製作状況

今回の対象

今回対象とする単語解析の部分の構成を以下に示す.

単語解析

流れとして,受信した単語を単語解析関数に入力し,戻り値として,解析結果,赤外線コマンド,音声ファイルのデータを返すものとする.これらの結果から,音声を流すかどうか,赤外線コマンドを送信するかどうかを決める.

単語の認識方法

さて,問題となる単語の認識方法であるが,ここでは,受信した単語に規定の単語が含まれている場合,その単語が発音されたものとする.具体例を以下に示す.

単語認識用データ

ここで,必須項目は,解析をするにあたり,必ず含まれている必要がある単語となる.必要項目は,列挙されているもののうち,ひとつでも含めばよいものである.空リストになっているものは,解析対象に含まれないことを意味する.それぞれの単語が見つかった場合の具体的な処理方法も別途データとして用意しておく必要がある.今回は,以下のようなデータを用意した.

解析時に利用するデータ

起動時に上記のデータを読み込み,処理に利用する.それぞれの項目の働きを以下に示す.

  • machine:機器の状態を管理するために利用する(Boolean 型).
  • json:読み込む JSON ファイル
  • cmd:JSON ファイルに記述されているコマンドのうち,実行するコマンド
  • voice:コマンド実行時に再生する音声ファイル
  • chkState:現在の状態から次の内部状態を決めるパラメータ

「machine」と「chkState」は関連しており,例えば,テレビがついているのに「テレビつけて」と発声した場合,ほとんどのテレビはオン/オフが同一ボタンであるため,テレビの電源がオフになってしまう.これを防ぐために,現状のテレビの状態を監視しておく必要がある.その際に利用するのが「machine」の部分となる.

また,誤認識防止のため,過去 1 回前までの単語のキーを記憶しておくようにしている.ただ,これに伴い,電気を明るくしたり,(ここにはないが)テレビの音量を上げる場合に必要となる「連続動作」ができなくなる.連続動作の対象となるものは,今回認識した単語を記憶しないようにしたいため,「chkState」を利用する.「chkState」は他にも,テレビがオンのとき「テレビをオンにして」という命令を棄却するのにも利用している.

内部状態を決めるパラメータの定義

現状,「chkState」では,1 Byte のデータを用いて,いくつかの状態を表せるようになっている.その内訳を以下に示す.

内部状態の内訳

これらのうち,予約済みのものを以下に示す.

予約済みの内部状態パラメータ

単語ファイルの更新が追いついていないが,最終的には,「Ok,Google」と話しかけたら認識開始,「ありがとう(仮)」と話しかけたら認識終了としようと考えている.

単語解析コード

上記の設定を実装したものを以下に示す.ここでは,「configuration.py」として保存する.また,実行には,「jsonData」のディレクトリに対象とするデータが追加されている必要がある.

~/juliusKit $ touch configuration.py
~/juliusKit $ vim configuration.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# configuration.py

import json
from VR_ConstClass import CONST_CLASS

class voiceConfigClass():
    def __init__(self):
        # 定数定義(public,結果解析で利用するため,外部から参照可能にしている)
        self.juliusReturnState = {
            True:      int('0x00', 16),
            False:     int('0x01', 16),
            'weather': int('0x02', 16)
        }

        # コンフィグ情報のマスク処理用パラメータ
        self.__maskDict = {'upper': int('0xF0', 16), 'lower': int('0x0F', 16)}
        # 機器の状態(True: 起動状態,False: 停止状態)
        self.__state = {'julius': False}
        # Julius の処理対象
        self.__JuliusConfig = {
            'invalid':     int('0x01', 16), # 無効値
            'wakeUp':      int('0xFF', 16), # 命令解析開始
            'sleep':       int('0xF8', 16), # 命令解析終了
            'weather':     int('0xF0', 16), # 天気予報読み上げ
            'init':        int('0xF1', 16), # 状態変数の初期化
            'active':      int('0x10', 16), # 実行中
            'stopped':     int('0x80', 16), # 停止中
            'update':      int('0x01', 16), # 状態を更新する
            'delPrevWord': int('0x02', 16)  # 直前の単語を記憶しない
        }
        # 語彙情報(定数)
        self.__confParamFile = 'confParam.json' # 図に示した「解析時に利用するデータ」が格納されている JSON ファイル
        self.__vocabInfo = None
        # 語彙リスト(図に示した「単語認識用データ」)
        # key: 単語ラベル
        # val: 語彙候補(mandatory: 必須項目,required: 必要項目)
        self.__vocabListData = {
            'lightOn': {
                'mandatory': [str('電気')],
                'required': [str('つけて'), str('オン')]
            },
            'lightOff': {
                'mandatory': [str('電気')],
                'required':  [str('オフ'), str('切って')]
            },
            'lightUp': {
                'mandatory': [str('明るくして')],
                'required':  []
            },
            'lightDown': {
                'mandatory': [str('暗くして')],
                'required':  []
            },
            'nightLight': {
                'mandatory': [str('こだまにして')],
                'required':  []
            },
            'TVOn': {
                'mandatory': [str('テレビ')],
                'required':  [str('つけて'), str('オン')]
            },
            'TVOff': {
                'mandatory': [str('テレビ')],
                'required':  [str('オフ'), str('切って')]
            }
        }
        # 作業用変数
        self.__jsonData = {}
        self.__prevWordIdx = ''
        self.__initConfigData()

    # 状態変数の初期化
    def __initStat(self, invalidList):
        keyList = [key for key, _ in self.__state.items()]
        for key in list(set(keyList) - set(invalidList)):
            self.__state[key] = False

    # 初期化処理関数
    def __initConfigData(self):
        # config データの取得
        jsonFileList = []
        machineTypeList = []
        # json file から config 情報を読み込む
        with open(CONST_CLASS.JSON_DIR + self.__confParamFile, 'r') as fin:
            self.__vocabInfo = json.load(fin)

        for key in self.__vocabInfo.keys():
            useMachine = self.__vocabInfo[key]['machine'].lower()
            self.__vocabInfo[key]['machine'] = useMachine
            jsonFileList.append(self.__vocabInfo[key]['json'])
            machineTypeList.append(useMachine)

        # 状態変数のキーの追加
        for key in list(set(machineTypeList)):
            self.__state[key] = False

        # json file の読み込み(list(set(---)) で重複を削除)
        for targetJsonFile in list(set(jsonFileList)):
            try:
                with open(CONST_CLASS.JSON_DIR + targetJsonFile, 'r') as fin:
                    self.__jsonData[targetJsonFile] = json.load(fin)
            except:
                # 該当する json file が存在しない場合,処理せず読み飛ばす
                pass

    # julius の状態の設定
    def setJuliusState(self, status):
        retVal = self.__state['julius']
        self.__state['julius'] = status
        return retVal

    # 状態の出力
    def getStatus(self):
        return [(key, self.__state[key]) for key in sorted(list(self.__state.keys()))]

    # 合致する単語を探す
    def __findMatchWord(self, wordData):
        retWordIdx = None

        # 単語の一覧から合致するパターンを抽出
        for key, wordDictList in self.__vocabListData.items():
            mandatoryData = wordDictList['mandatory']
            requiredData = wordDictList['required']
            # 必須項目の単語が含まれているか調査
            isExistMandatory = True
            for targetWord in mandatoryData:
                if targetWord not in wordData:
                    isExistMandatory = False
                    break
            # 必要項目の単語が含まれているか調査
            if len(requiredData) == 0:
                isExistRequired = True
            else:
                isExistRequired = False
                for targetWord in requiredData:
                    if targetWord in wordData:
                        isExistRequired = True

            if isExistMandatory and isExistRequired:
                retWordIdx = key
                break
        return retWordIdx

    # コマンドの実行可能判定
    def chkCmdExection(self, wordData):
        local_tmpVal = False
        returnState = self.juliusReturnState[local_tmpVal]
        retCmd = None
        wordIdx = self.__findMatchWord(wordData)
        retAudio = ''

        # 単語が候補に存在するかつ,前回と同じ単語でない
        if wordIdx is not None and wordIdx != self.__prevWordIdx:
            useMachine = self.__vocabInfo[wordIdx]['machine']
            useState = int(self.__vocabInfo[wordIdx]['chkState'], 16)
            useJsonFile = self.__vocabInfo[wordIdx]['json']
            useCmd = self.__vocabInfo[wordIdx]['cmd']
            tmpAudio = self.__vocabInfo[wordIdx]['voice']
            useAudio = {False: '', True: tmpAudio, 'weather': tmpAudio}

            try:
                retCmd = self.__jsonData[useJsonFile][useCmd]
            except:
                # コマンドが存在しない場合
                retCmd = None
            # 命令解析開始メッセージの場合
            if useState == self.__JuliusConfig['wakeUp']:
                self.setJuliusState(True)
                local_tmpVal = True
                self.__prevWordIdx = ''
            elif self.__state['julius']:
                # 命令解析終了メッセージの場合
                if useState == self.__JuliusConfig['sleep']:
                    self.setJuliusState(False)
                    local_tmpVal = True
                    self.__prevWordIdx = ''
                # Julius の初期化要求の場合
                elif useState == self.__JuliusConfig['init']:
                    # 状態を初期化
                    self.__initStat(['julius'])
                    local_tmpVal = True
                    self.__prevWordIdx = ''
                # 天気予報読み上げ要求の場合
                elif useState == self.__JuliusConfig['weather']:
                    local_tmpVal = 'weather'
                    self.__prevWordIdx = ''
                # 無効値でない場合
                elif useState != self.__JuliusConfig['invalid']:
                    # 入力コマンドの動作条件を確認し,コマンドを実行するか決める
                    upperInfo = (useState & self.__maskDict['upper'])
                    lowerInfo = (useState & self.__maskDict['lower'])

                    if upperInfo == self.__JuliusConfig['active']:
                        # [期待値]対象機器が動作中
                        local_tmpVal = self.__state[useMachine]
                    elif upperInfo == self.__JuliusConfig['stopped']:
                        # [期待値]対象機器が停止中
                        local_tmpVal = not self.__state[useMachine]
                    else:
                        # [期待値]なし で初期化
                        local_tmpVal = True

                    # コマンドを実行する場合
                    if local_tmpVal:
                        # 現在の入力単語を記録
                        self.__prevWordIdx = wordIdx
                        # 下位 4 ビットを確認し処理する
                        if lowerInfo == self.__JuliusConfig['update']:
                            # 状態を反転
                            self.__state[useMachine] = not self.__state[useMachine]
                        elif lowerInfo == self.__JuliusConfig['delPrevWord']:
                            # 記録した単語を削除
                            self.__prevWordIdx = ''
            retAudio = useAudio[local_tmpVal]
            returnState = self.juliusReturnState[local_tmpVal]
        return returnState, retCmd, retAudio

自分が利用している「confParam.json」ファイルを以下に示す.

{
    "lightOn": {
        "cmd": "all_light",
        "machine": "light",
        "json": "lightData.json",
        "voice": "lightOn.wav",
        "chkState": "0x81"
    },
    "lightOff": {
        "cmd": "light_off",
        "machine": "light",
        "json": "lightData.json",
        "voice": "lightOff.wav",
        "chkState": "0x11"
    },
    "lightUp": {
        "cmd": "up",
        "machine": "light",
        "json": "lightData.json",
        "voice": "lightUp.wav",
        "chkState": "0x12"
    },
    "lightDown": {
        "cmd": "down",
        "machine": "light",
        "json": "lightData.json",
        "voice": "lightDown.wav",
        "chkState": "0x12"
    },
    "nightLight": {
        "cmd": "night_light",
        "machine": "light",
        "json": "lightData.json",
        "voice": "nightLight.wav",
        "chkState": "0x11"
    },
    "TVOn": {
        "cmd": "power",
        "machine": "tv",
        "json": "TVData.json",
        "voice": "TVOn.wav",
        "chkState": "0x81"
    },
    "TVOff": {
        "cmd": "power",
        "machine": "tv",
        "json": "TVData.json",
        "voice": "TVOff.wav",
        "chkState": "0x11"
    }
}

長くなってしまったため,main 関数部分と解析結果部分に関しては,次回にまわす.main 関数部分が軸となって動作するため,これまでの関数を利用してスレッドを立ち上げつつ動作する.ここまでのディレクトリ構成を以下に示す.

~/juliusKit
   |--dictationKit_v4.3.1
       |--word.dic
       |--word.jconf
   |--grammarKit
   |   |--controller
   |        |--compile.sh
   |        |--mkdfa.pl
   |        |--mkfa
   |        |--utf8_controller.grammar
   |        |--utf8_controller.voca
   |--outYomi.sh
   |--word.yomi
   |--VR_ConstClass.py
   |--parseJuliusData.py
   |--webSocketClient.py
   |--configuration.py    # 追加部分
   |--jsonData            # 追加部分
   |   |--TVData.json
   |   |--confParam.json
   |   |--lightData.json
   |--wavFile             # 追加部分
         |--TVOff.wav
         |--TVOn.wav
         |--lightDown.wav
         |--lightOff.wav
         |--lightOn.wav
         |--lightUp.wav
         |--nightLight.wav

音声認識による赤外線機器の操作 その 5【認識結果パース編】

どうも,筆者です.

前回までで,赤外線操作ができた.ここからは,これらを組み合わせて音声認識結果から赤外線操作を行う.

前回までの記事は以下にある.

workspacememory.hatenablog.com

全体像

ここで,全体像を示しておく.細かい部分や他との関連は,各々のコードを使いつつ確認する.

全体像

現状の処理の流れとしては,以下のようになる.

  1. マイクから音声を拾う.
  2. Julius により音声認識を行い,認識結果を得る.
  3. 認識結果から,利用する赤外線データを選択する.
  4. 赤外線データを ADRSIR を用いて外部に出力する.

製作状況

現状では,音声認識結果と赤外線送信は機能としてある.(どちらも,外部ツールに頼っているため,自分で触った部分はほとんどないが...) 今回は,Julius の音声認識結果をパースし,パース結果(単語情報)から赤外線データを選択する部分までを解説する.

今回の対象

認識結果解析の中身

ここでは,認識結果の解析を行うための方法を考える.将来的に別の外部入力から単語情報を入力しても操作できるようにしたいため,以下のように処理を分割する.

  • Julius から単語情報を抽出する.
    • Julius の起動モードを変更することで,プログラムから結果を受信できる.
    • データの受信には socket 通信を用いる.
  • 単語情報から赤外線情報を選択する.
    • 単語の受信には WebSocket を用いる.

認識結果解析

Julius から単語情報を抽出

さて,Python を用いて実際にプログラミングをしていく.これまで,Julius を「-demo」モードで起動してきたが,プログラムからデータを受信するためには,「-module」モードで起動する必要がある.これは単に引数を変更するだけで対応できる. また,「-module」モードで起動した Julius は,localhost の 10500 番に結果を出力する仕様であるため,このホストとポートで入力待ちをすればよい.

グローバル変数の定義

今後,外部ツールや JSON ファイルを読み出すことになるため,必要なパスをグローバル変数として保持しておく.ここでは,以下のようなクラスをもつ Python スクリプトを作成した.ファイル名は「VR_ConstClass.py」である.

~/juliusKit $ touch VR_ConstClass.py
~/juliusKit $ vim VR_ConstClass.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# VR_ConstClass.py

class CONST_CLASS:
    # ==========================
    # = common const parameter =
    # ==========================
    ROOT_DIR = '/home/pi/juliusKit/' # root directory
    JSON_DIR = ROOT_DIR + 'jsonData/'  # json directory
    WAVE_DIR = ROOT_DIR + 'wavFile/'   # wave directory
    WEBSOCKET_HOST = 'localhost'       # WebSocket host name
    WEBSOCKET_PORT = 10510             # WebSocket port number

ここにあるように,最終的には発音に対し音声で対応できるようにしたいと考えている.

Julius の起動と接続の確立

Julius を起動と Julius に接続するまでの処理を記したスクリプト「parseJuliusData.py」を以下に示す.今後,これに変更を加えていく.

~/juliusKit $ touch parseJuliusData.py
~/juliusKit $ vim parseJuliusData.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# parseJuliusData.py

import subprocess as sb
import time, socket, threading, select
from VR_ConstClass import CONST_CLASS

class parseJuliusDataClass():
    def __init__(self):
        # julius ホスト名
        self.__juliusHost = 'localhost'
        # julius ポート番号
        self.__juliusPort = 10500
        # タイムアウトの最大時間 [sec]
        self.__maxTimeOut = 20.0
        # 受信データサイズ
        self.__receiveSize = 2048
        # Julius サーバ起動コマンド
        #jconfFile = 'dictationKit_v4.3.1/word.jconf' # 単語認識版
        jconfFile = 'grammarKit/control.jconf' # 文法認識版
        self.__juliusServerCmd = [
            '/usr/local/bin/julius', '-C',
            CONST_CLASS.ROOT_DIR + jconfFile,
            '-module', '-nostrip'
        ]
        self.__process = None
        self.__socket = None
        self.__isRunning = True

    # Julius の起動
    def __wakeUpJuliusServer(self):
        # Julius Server の起動
        self.__process = sb.Popen(self.__juliusServerCmd, stdout=sb.DEVNULL, stderr=sb.DEVNULL)

    # julius サーバに接続
    def __connectionJuliusServer(self, arg_maxTimeout):
        while True:
            try:
                # ソケットの生成
                self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.__socket.connect((self.__juliusHost, self.__juliusPort))
                break
            except:
                self.__socket = None
                time.sleep(arg_maxTimeout)

    # 実行用関数
    def __parseText(self):
        local_maxTimeOut = 3
        # Julius の起動
        self.__wakeUpJuliusServer()
        # サーバに接続
        self.__connectionJuliusServer(local_maxTimeOut)

subprocess や socket の使い方は各自調べてほしい.

WebSocket 利用のための準備

今回は,Julius から受信したデータを単語解析処理に送信する際に,WebSocket を利用するため,コマンドラインで以下をインストールする.

~/juliusKit $ sudo pip3 install git+https://github.com/dpallot/simple-websocket-server.git
~/juliusKit $ sudo pip3 install websocket-client

インストール後,まずは,client 側のコードを作成する.以下のように「webSocketClient.py」として保存する.

~/juliusKit $ touch webSocketClient.py
~/juliusKit $ vim webSocketClient.py # エディタは自分の使いやすいものを利用する
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# webSocketClient.py

from websocket import create_connection

class wsClientClass():
    def __init__(self, host, port):
        self.__connectAddr = 'ws://{0}:{1}/'.format(host, port)

    def sendMsg(self, message):
        try:
            ws = create_connection(self.__connectAddr)
            ws.send(message)
            ws.close()
        except:
            pass

WebSocket の処理の追加

上記のコードを先程のコードに組み込むと,以下のようになる.

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# parseJuliusData.py

import subprocess as sb
import webSocketClient as wsClient # 追加部分
import time, socket, threading, select
from VR_ConstClass import CONST_CLASS

class parseJuliusDataClass():
    def __init__(self):
        # julius ホスト名
        self.__juliusHost = 'localhost'
        # julius ポート番号
        self.__juliusPort = 10500
        # タイムアウトの最大時間 [sec]
        self.__maxTimeOut = 20.0
        # 受信データサイズ
        self.__receiveSize = 2048
        # Julius サーバ起動コマンド
        #jconfFile = 'dictationKit_v4.3.1/word.jconf' # 単語認識版
        jconfFile = 'grammarKit/control.jconf' # 文法認識版
        self.__juliusServerCmd = [
            '/usr/local/bin/julius', '-C',
            CONST_CLASS.ROOT_DIR + jconfFile,
            '-module', '-nostrip'
        ]
        self.__process = None
        self.__socket = None
        self.__isRunning = True

    # Julius の起動
    def __wakeUpJuliusServer(self):
        # Julius Server の起動
        self.__process = sb.Popen(self.__juliusServerCmd, stdout=sb.DEVNULL, stderr=sb.DEVNULL)

    # julius サーバに接続
    def __connectionJuliusServer(self, arg_maxTimeout):
        while True:
            try:
                # ソケットの生成
                self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.__socket.connect((self.__juliusHost, self.__juliusPort))
                break
            except:
                self.__socket = None
                time.sleep(arg_maxTimeout)

    # 実行用関数
    def __parseText(self):
        local_maxTimeOut = 3
        # Julius の起動
        self.__wakeUpJuliusServer()
        # サーバに接続
        self.__connectionJuliusServer(local_maxTimeOut)
        # WebSocket Client のインスタンス生成
        client = wsClient.wsClientClass(
            CONST_CLASS.WEBSOCKET_HOST,
            CONST_CLASS.WEBSOCKET_PORT
        )

受信処理関数の定義

実際に Julius からデータを受信し処理する部分を以下に示す.

       readData = ''
        while self.__isRunning:
            # ソケットが読み込み可能状態になるまで待機
            inputReady, _, _ = select.select([self.__socket], [], [], self.__maxTimeOut)

            # 空リストの場合
            if len(inputReady) == 0:
                # タイムアウトしたため,スリープモードへ遷移
                # ToDo: タイムアウト処理
                pass
            else:
                # 読み込み可能状態のソケットが含まれる場合
                if self.__socket in inputReady:
                    readData += str(self.__socket.recv(self.__receiveSize).decode('utf-8'))

                    # 「認識結果」のデータがある場合
                    if '</RECOGOUT>\n.' in readData:
                        wordData = ''

                        for line in readData.split('\n'):
                            # WORD という単語を探す
                            searchStr = 'WORD="'
                            matchIndex = line.find(searchStr)

                            if matchIndex >= 0:
                                # 単語が存在する場合,その単語を抽出
                                startIdx = matchIndex + len(searchStr)
                                wordData += str(line[startIdx:line.find('"', startIdx)])
                        # 単語の入力があった場合
                        if wordData != '':
                            # サーバにデータを送信
                            client.sendMsg(wordData)
                        readData = ''

受信処理関数のスレッド化

また,これだけが動作するわけではなく,今後作成するほかのプログラムと並列に動作してほしいため,スレッド化する.その処理を加えたものを以下に示す.

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# parseJuliusData.py

import subprocess as sb
import webSocketClient as wsClient
import time, socket, threading, select
from VR_ConstClass import CONST_CLASS

class parseJuliusDataClass():
    def __init__(self):
        # julius ホスト名
        self.__juliusHost = 'localhost'
        # julius ポート番号
        self.__juliusPort = 10500
        # タイムアウトの最大時間 [sec]
        self.__maxTimeOut = 20.0
        # 受信データサイズ
        self.__receiveSize = 2048
        # Julius サーバ起動コマンド
        #jconfFile = 'dictationKit_v4.3.1/word.jconf' # 単語認識版
        jconfFile = 'grammarKit/control.jconf' # 文法認識版
        self.__juliusServerCmd = [
            '/usr/local/bin/julius', '-C',
            CONST_CLASS.ROOT_DIR + jconfFile,
            '-module', '-nostrip'
        ]
        self.__process = None
        self.__socket = None
        self.__isRunning = True
        self.__thread = threading.Thread(target=self.__parseText)

    # スレッドを開始する
    def startThread(self):
        self.__isRunning = True
        self.__thread.start()

    # スレッドを停止する
    def stopThread(self):
        self.__isRunning = False   # 停止イベントを設定
        self.__thread.join()       # スレッドが停止するのを待つ
        self.__process.terminate() # サブプロセスを終了する
        self.__socket.close()      # ソケットを閉じる

    # Julius の起動
    def __wakeUpJuliusServer(self):
        # Julius Server の起動
        self.__process = sb.Popen(self.__juliusServerCmd, stdout=sb.DEVNULL, stderr=sb.DEVNULL)

    # julius サーバに接続
    def __connectionJuliusServer(self, arg_maxTimeout):
        while True:
            try:
                # ソケットの生成
                self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.__socket.connect((self.__juliusHost, self.__juliusPort))
                break
            except:
                self.__socket = None
                time.sleep(arg_maxTimeout)

    # 実行用関数
    def __parseText(self):
        local_maxTimeOut = 3
        # Julius の起動
        self.__wakeUpJuliusServer()
        # サーバに接続
        self.__connectionJuliusServer(local_maxTimeOut)
        # WebSocket Client のインスタンス生成
        client = wsClient.wsClientClass(
            CONST_CLASS.WEBSOCKET_HOST,
            CONST_CLASS.WEBSOCKET_PORT
        )

        readData = ''
        while self.__isRunning:
            # ソケットが読み込み可能状態になるまで待機
            inputReady, _, _ = select.select([self.__socket], [], [], self.__maxTimeOut)

            # 空リストの場合
            if len(inputReady) == 0:
                # タイムアウトしたため,スリープモードへ遷移
                # ToDo: タイムアウト処理
                pass
            else:
                # 読み込み可能状態のソケットが含まれる場合
                if self.__socket in inputReady:
                    readData += str(self.__socket.recv(self.__receiveSize).decode('utf-8'))

                    # 「認識結果」のデータがある場合
                    if '</RECOGOUT>\n.' in readData:
                        wordData = ''

                        for line in readData.split('\n'):
                            # WORD という単語を探す
                            searchStr = 'WORD="'
                            matchIndex = line.find(searchStr)

                            if matchIndex >= 0:
                                # 単語が存在する場合,その単語を抽出
                                startIdx = matchIndex + len(searchStr)
                                wordData += str(line[startIdx:line.find('"', startIdx)])
                        # 単語の入力があった場合
                        if wordData != '':
                            # サーバにデータを送信
                            client.sendMsg(wordData)
                        readData = ''

ここまでのディレクトリ構成を以下に示す.

~/juliusKit
   |--dictationKit_v4.3.1
       |--word.dic
       |--word.jconf
   |--grammarKit
   |   |--controller
   |        |--compile.sh
   |        |--mkdfa.pl
   |        |--mkfa
   |        |--utf8_controller.grammar
   |        |--utf8_controller.voca
   |--outYomi.sh
   |--word.yomi
   |--VR_ConstClass.py   # 追加部分
   |--parseJuliusData.py # 追加部分
   |--webSocketClient.py # 追加部分

長くなったため,一旦ココまでとする.今後,単語解析処理側のプログラムを作成する.