rohaniのブログ

ゆるっと自然言語処理奴。ときどき工作系バイト。

Raspberry Pi 3b+ と Julius で単語カウンターを作った

「喋っている会話の中で言われた名前の回数をカウントするものがほしい。」

「おー面白そ」

ってことで作りました。

要件

  • 自然発話による会話を音声認識し、発話中の対象固有名詞をリアルタイムに検出する
  • 対象固有名詞の回数をカウントし、コマンドライン出力
  • 回数をLCD表示したかった(けどできなかった。申し訳ない)

環境

HDMIケーブルとディスプレイとWi-Fi以外は借り物なので型番とかはよく分からない。

しかし、こう頻繁にRaspberry Piを借りれる(しかも同じ人じゃない)環境とは。。。ありがてぇのう。。

Juliusとは?

Julius は、C言語で書かれたオープンソースの高性能な汎用大語彙連続音声認識エンジン。一般のPCやスマートフォン上でほぼ実時間で実行できる軽量さとコンパクトさを持ち、単語辞書や言語モデル等を各自でよしなに弄れる自由度もある。

julius.osdn.jp

(できればオフラインで)実時間音声言語処理したいなと思うと、Juliusが候補に上がってくるかなと思う。

ラズパイでも動く。

作業記録

作業は以下の記事を参考に行った。

マイクの準備

マイクを接続する

USBマイクをどこかしらのソケットに刺す。 下記のコマンドで認識されているかどうかが分かる。

$ lsusb
Bus 001 Device 00x: ~~~(認識されているもの)~~~
...

音声を録音してみる

  • arecord -lコマンドで、carddeviceの番号を確かめる。マイクを指定するのに使う。
  • arecord -D plughw:CARDNUM,DEVICENUM test.wavコマンドで、録音ができる。CARDNUMとDEVICENUMは先程確かめた番号。
  • aplay test.wavコマンドで、録音した音声を再生する。
  • 音量が小さい場合はalsamixerコマンドで大きくする。

Juliusを実行する際にも、このcarddeviceの番号が必要なので、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.confsudoで編集する。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=-2options 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だけが心残り。

カウンターと化したラズパイは今朝、無事にドナドナされていった。