Raspberry Pi 3b+ と Julius で単語カウンターを作った
「喋っている会話の中で言われた名前の回数をカウントするものがほしい。」
「おー面白そ」
ってことで作りました。
要件
環境
- Raspberry Pi 3b+
- 周辺機器(マウス・キーボード・HDMIケーブル・ディスプレイ・WiFi)
- USBマイク(FIFINE ファイファイン USB コンデンサーマイク PC PS4 通話用 Skype 配信 単一指向性 音量調節可能 マイクスタンド付属 Windows/Mac対応 ブラック K669B)
- LCD
HDMIケーブルとディスプレイとWi-Fi以外は借り物なので型番とかはよく分からない。
しかし、こう頻繁にRaspberry Piを借りれる(しかも同じ人じゃない)環境とは。。。ありがてぇのう。。
Juliusとは?
Julius は、C言語で書かれたオープンソースの高性能な汎用大語彙連続音声認識エンジン。一般のPCやスマートフォン上でほぼ実時間で実行できる軽量さとコンパクトさを持ち、単語辞書や言語モデル等を各自でよしなに弄れる自由度もある。
(できればオフラインで)実時間音声言語処理したいなと思うと、Juliusが候補に上がってくるかなと思う。
ラズパイでも動く。
作業記録
作業は以下の記事を参考に行った。
- ラズパイ3+Juliusで音声認識 – FRONT
簡潔かつ豊富な情報が記載されている。メインで参考にさせて頂きました。 - Raspberry Pi+Juliusで音声を認識する - Qiita
とても丁寧な解説が記載されている。マイクの準備の際に参考にさせて頂きました。 - Raspberry piでjuliusを使用して音声認識 - Qiita
今回のタスクに近い内容が記載されている。辞書づくりからの流れを参考にさせて頂きました。
マイクの準備
マイクを接続する
USBマイクをどこかしらのソケットに刺す。 下記のコマンドで認識されているかどうかが分かる。
$ lsusb Bus 001 Device 00x: ~~~(認識されているもの)~~~ ...
音声を録音してみる
arecord -l
コマンドで、card
とdevice
の番号を確かめる。マイクを指定するのに使う。arecord -D plughw:CARDNUM,DEVICENUM test.wav
コマンドで、録音ができる。CARDNUMとDEVICENUMは先程確かめた番号。aplay test.wav
コマンドで、録音した音声を再生する。- 音量が小さい場合は
alsamixer
コマンドで大きくする。
Juliusを実行する際にも、このcard
とdevice
の番号が必要なので、Juliusが常に同じ設定を使用できるように、環境変数として設定しておく。
vi ~/.profile
で~/.profileを開いて、export ALSADEV=“plughw:CARDNUM,DEVICENUM"
を最終行とかに追記
ここで、export ALSADEV=hw:CARDNUM
を追記するという作法もあったが、どちらがいいのか。
追記:この行を消して再起動してみたが、問題なくマイクを使うことができた。謎が深まる。
マイクの優先順位を上げる
下記コマンドでラズパイが認識しているサウンドデバイスを確認する。
$ cat /proc/asound/modules 0 snd_bcm2835 1 snd_usb_audio
数字が小さい方が優先順位が高いので、この状態だと、既存の内蔵オーディオモジュールが優先されている状態であると分かる。 このままだと、Juliusを動かしたときに音声を扱えない。ほんとに。
そこで、/lib/modprobe.d/aliases.conf
をsudo
で編集する。sudo vi /lib/modprobe.d/aliases.conf
とか。
options snd-usb-audio index=-2
上を、下記のようにコメントアウト&追記
# options snd-usb-audio index=-2 options snd slots=snd_usb_audio,snd_bcm2835 options snd_usb_audio index=0 options snd_bcm2835 index=1
ここで、単純にoptions snd-usb-audio index=-2
をoptions snd-usb-audio index=0
に変えるという記事もあったが、私の環境ではそれだけでは動かなかった。
Juliusの環境構築
Raspberry Pi へのJuliusのダウンロード&インストール手順は下記。
ちなみに、MacOSへJuliusをインストールする際は、これをするとHomebrew
の領域を侵してややこしいことになるのでHomebrew
で入れることをおすすめします。ほんとに。
$wget https://github.com/julius-speech/julius/archive/v4.4.2.1.tar.gz $tar zxvf v4.4.2.1.tar.gz $cd julius-4.4.2.1 $./configure $make $sudo make install
続いて、ディクテーション実行キットをダウンロード&解凍
$wget https://osdn.net/dl/julius/dictation-kit-v4.4.zip $unzip dictation-kit-v4.4.zip
さて、Juliusはossのキャラクター型デバイスの1つの/dev/dsp
を使うのだけれど、これが新しめのカーネルには標準搭載されていない。
Raspberry Pi 3b+の場合は、下記のコマンドで代わりのものをインストールする。
$sudo apt-get install osspd-alsa
音声認識を試してみる
$cd ~/julius-4.4.2.1/dictation-kit-v4.4 $julius -C main.jconf -C am-gmm.jconf -demo
<<<please speak>>>
などと表示されたら喋る。何かしらの文字列が表示されたら、認識できている。
Juliusの辞書づくり
ユーザ辞書にある単語のみで単語認識する方向に決めた経緯
デモをしてみると分かるのだけれど、デフォルトで入っているサイズの語彙で、かつ文レベルでの音声認識をさせてみると、その精度はあんまりよろしくない。
「コードフォーミカワ」というコミュニティ名を何度か試してみた結果は以下の通り。これでは、認識結果の曖昧性を吸収するのは難しそう。
- Pass1_best : 高度等に変わっ、sentence1: コードっぽい顔。
- Pass1_best : コードと美加は、sentence1: コードフォー三川。
- Pass1_best : コードと美加は、sentence1: 行動、青海川。
- Pass1_best : こうと、もう、sentence1: 行動、法皇。
そこで、一般的に用いられる方法として、単語の数を絞り込む事を考える。また、今回検出したいのは1単語だけなので、文法等は定義せず、1単語の認識を行うことにした。
辞書づくり
実装手順は、こちらのサイト(Raspberry pi上の音声認識(julius)認識率向上[julius辞書作成] - Qiita)を参考にした。重複するので詳細は割愛するが、流れはだいたい下記のようになる。
- 単語とよみがなのリスト作成
- 辞書形式に変更
- Julius設定ファイルの作成
- 動作確認
意外と時間はかからない。
辞書には今回の対象固有名詞「コードフォーミカワ」の他にも、いくつか単語を登録しておいた。
- こーどふぉーみかわ:認識したい単語
- こーど、ふぉー、みかわ:部分文字列一致を検出しないように別単語として登録
- こーと、こーとを:似ているようで違う単語を登録しておくことで、認識したい単語とそれ以外の境界が明確になればいいなと思った
- こーどふぉー〇〇:〇〇、にいくつか単語を入れてみた。人名を入れてしまったので伏せておく。
- 〇〇:関係ない単語もいくつか入れてみた。人名を入れてしまったので伏せておく。
結構いい感じに「コードフォーミカワ」の検出をしてくれるようになりました。
対象固有名詞の検出とカウント
最後に、Juliusの音声認識結果を受け取り、「コードフォーミカワ」の回数をカウントする部分を実装した。
実装はこちらのサイト(https://qiita.com/fishkiller/items/c6c5c4dcd9bb8184e484)を参考にPythonで行った。
# -*- coding: utf-8 -*- #import subprocess import socket import string import os import random import time host = "localhost" port = 10500 time.sleep(5) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) print() print("Code for MIKAWAカウンターの準備ができたじゃんね") print("ほいだもんで、何か喋ってみりん〜") print() def mikawaSoul(c4m_cnt): if c4m_cnt == 5: print("Code for MIKAWAへの愛を感じるじゃんね") if c4m_cnt == 10: print("Code for MIKAWA、どおもしろいらー") def aizuchi(num): if num < 45: print("うん") elif (45 <= num) and (num < 50): print("聞いとるでやー") elif (50 <= num) and (num < 55): print("ほっか") elif (55 <= num) and (num < 60): print("ほっかー") elif (60 <= num) and (num < 65): print("ほいで?") elif (65 <= num) and (num < 70): print("ほだらー") elif 70 <= num: print("うんうん") try: data = "" c4m_cnt = 0 while True: while (1): if '</RECOGOUT>\n.' in data: #print(data) #print() strTemp = "" for line in data.split('\n'): index = line.find('WORD="') if index != -1: line = line[index+6:line.find('"',index+6)] strTemp += str(line) if strTemp == 'こーどふぉーみかわ': #print(strTemp) c4m_cnt += 1 print("Code for MIKAWA: " + str(c4m_cnt)) mikawaSoul(c4m_cnt) elif strTemp != '': #print(strTemp) #aizuchi(random.randint(0,100)) aizuchi(45) data = "" else: data += str(sock.recv(1024).decode('utf-8')) except KeyboardInterrupt: print("\n\nCode for MIKAWA: "+str(c4m_cnt)) print("\n\n===!!ATTENTION!!===") print("FOR TO KILL JULIUS PROCESS") print("CHECK the process id of JULIUS by `ps` command") print("KILL JULIUS PROCESS by `kill xxxx` command. xxxx is number of JULIUS PROCESS") print("===!!ATTENTION!!===\n\n")
三河弁
一応地元の人に聞いたりしましたが、私自身三河出身じゃないので間違っているかもしれません。
じゃんだらりん。
感想
「楽しかったー」笑
思ったよりいい感じに検出してくれるようになったので嬉しかった。 この方法でうまく言ったのは、恐らく対象がそこそこユニークな感じの固有名詞だったことが幸いしたのではないかなと思う。 LCDだけが心残り。
カウンターと化したラズパイは今朝、無事にドナドナされていった。