作業中のメモ

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

音声認識による赤外線機器の操作 その 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 # 追加部分

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

音声認識による赤外線機器の操作 その 4【文法辞書作成編】

どうも,筆者です.

以前,辞書を作成したが,決まった単語のみの組み合わせだけだった.今回は,もう少し柔軟に対応できるように辞書を作成する.

関連ページ

workspacememory.hatenablog.com

文法認識の考え方

復習となるが,以前は,以下のような単語のパターンを入力していた.

電気つけて
電気オフ
明るくして
暗くして
こだまにして
テレビオン
テレビ切って

この場合,「電気オフ」と「テレビ切って」や「電気つけて」と「テレビオン」等を使い分ける必要があった.この場合,命令をすべて覚える必要があるが,以下のように認識できれば多少の揺れにも対応できる.

# 電気をつけたい場合
電気つけて
電気をつけて
電気オン
# テレビを付けたい場合
テレビつけて
テレビをつけて
テレビオン

上のパターンは「[家電の名前][操作方法]」という形式で置き換えられる. ここから,家電の名前と操作方法を定義し,これらに対応する文法を定義することで,上記のパターンに対応できることになる.

# 家電の名前
電気
テレビ
# 操作方法
つけて
オン
# 文法
[家電の名前][操作方法]

Julius では,この考え方を用いて音声認識が行える.

記述文法認識キット

今回は,記述文法認識キット(grammar kit)を利用して,文法認識を行う.

語彙の作成

「~/juliusKit/grammarKit」以下に「controller」というディレクトリを作成し,この中で作業を行う.まずは,単語とその読みを「yomiWord.list」というファイルに定義する.この時,上記で用いる単語の組み合わせを考え,定義する.

~ $ pushd ~/juliusKit/grammarKit
~/juliusKit/grammarKit $ mkdir controller
~/juliusKit/grammarKit $ pushd controller
~/juliusKit/grammarKit/controller $ touch yomiWord.list
~/juliusKit/grammarKit/controller $ vim yomiWord.list # エディタは自分の使いやすいものを利用する
# === ここから ===
電気  でんき
テレビ  テレビ
つけて  つけて
オン    おん
オフ    おふ
切って  きって
明るくして あかるくして
暗くして    くらくして
こだまにして  こだまにして
# === ここまで ===

そして,以下のコマンドで,読み方をローマ字形式に変換し,結果を「utf8_controller.voca」に格納する.

~/juliusKit/grammarKit/controller $ cat yomiWord.list | iconv -f utf8 -t eucjp | perl ../bin/linux/yomi2voca.pl | nkf -wLu > utf8_controller.voca
### 出力結果 ###
# 電気   d e N k i
# テレビ    t e r e b i
# つけて    ts u k e t e
# オン   o N
# オフ   o f u
# 切って    k i q t e
# 明るくして  a k a r u k u sh i t e
# 暗くして k u r a k u sh i t e
# こだまにして   k o d a m a n i sh i t e

ここから,出力した「utf8_controller.voca」を編集していく.やることとしては,先程「家電の名前」や「操作方法」という呼び名をつけたように,出力データにも名前を付ける.名前を付ける際は「% [名称]」という形式にする.今回は,以下のようにした.

# この行はコメント扱いとなる
# 家電データ
% KADEN
電気  d e N k i
テレビ   t e r e b i
# 追加の助詞
を w o
# 家電の操作
% MODE_KADEN
つけて   ts u k e t e
オン  o N
オフ  o f u
切って   k i q t e
# 電気に対する操作
% MODE_LIGHT
明るくして a k a r u k u sh i t e
暗くして    k u r a k u sh i t e
こだまにして  k o d a m a n i sh i t e
# 文頭と文末を表すデータ
% NS_B
silB    silB
% NS_E
silE    silE

ここで,必ず必要なのが末尾の「文頭と文末を表すデータ」である.これは,Julius で入力の無音区間と休止区間を表すために必要となる.

文法ファイルの作成

先程作成した語彙ファイルを利用して,文法ファイルを作成する.辞書ファイルの形式を以下に示す.

# 辞書ファイルの形式
# 文法は複数定義できる
S : NS_B [文法1] NS_E # 文法1
S : NS_B [文法2] NS_E # 文法2
S : NS_B [文法3] NS_E # 文法3

今回は,家電の操作と電気に対する操作を定義することを考える.この場合,以下のような文法を作成すればよい.

S : NS_B [家電の名前] [操作方法] NS_E
S : NS_B [電気に対する操作] NS_E

これを上に挙げた名称で置き換えれば,文法を作成したことになる.

S : NS_B KADEN MODE_KADEN NS_E
S : NS_B MODE_LIGHT NS_E

これでも良いが,「テレビつけて」と「テレビをつけて」の両方に対応するため,上の文法を少し弄る.

S : NS_B KADEN_ MODE_KADEN NS_E
S : NS_B MODE_LIGHT NS_E
# KADEN_ の定義(「_」が付いていることに注意)
KADEN_  : KADEN
KADEN_  : KADEN WO

これを,「utf8_controller.grammar」として保存する.ここまでのディレクトリ構成を以下に示す.

~/juliusKit
   |--dictationKit_v4.3.1
       |--word.dic
       |--word.jconf
   |--grammarKit
   |   |--controller
   |        |--utf8_controller.grammar
   |        |--utf8_controller.voca
   |--outYomi.sh
   |--word.yomi

# utf8_controller.grammar の中身
# === ここから ===
S : NS_B KADEN_ MODE_KADEN NS_E
S : NS_B MODE_LIGHT NS_E
# KADEN_ の定義(「_」が付いていることに注意)
KADEN_  : KADEN
KADEN_  : KADEN WO
# === ここまで ===

語彙ファイルと文法ファイルから Julius で利用するデータを作成

語彙ファイルと文法ファイルから Julius で利用するデータを作成する.そのために,Julius をインストールした際のディレクトリから以下の 2 つをコピーしてくる.

~/juliusKit/grammarKit/controller $ cp -f ~/julius/gramtools/mkdfa/mkfa-1.44-flex/mkfa ./
~/juliusKit/grammarKit/controller $ cp -f ~/julius/gramtools/mkdfa/mkdfa.pl ./

そして,データ生成用の Shell Script を作成する.ここでは,「compile.sh」という名前で保存する.

~/juliusKit/grammarKit/controller $ touch compile.sh
~/juliusKit/grammarKit/controller $ vim compile.sh # エディタは自分の使いやすいものを利用する
# === ここから ===
#!/bin/bash
# compile.sh

controller="controller"
grammarFile=utf8_${controller}.grammar
vocaFile=utf8_${controller}.voca

nkf -sLw ${grammarFile} > ${controller}.grammar
nkf -sLw ${vocaFile} > ${controller}.voca
perl mkdfa.pl ${controller}
rm -f ${controller}.grammar ${controller}.voca
# === ここまで ===

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

~/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

後は,Shell Script に実行権限をつけ,以下のように実行すると,メッセージが出力され,「controller.dfa」,「controller.term」,「controller.dict」が生成される.

~/juliusKit/grammarKit/controller $ chmod +x compile.sh
~/juliusKit/grammarKit/controller $ ./compile.sh
### 出力結果
# controller.grammar has xx rules
# controller.voca    has xx categories and yy words
# ---
# Now parsing grammar file
# Now modifying grammar to minimize states[ww]
# Now parsing vocabulary file
# Now making nondeterministic finite automaton[zz/zz]
# Now making deterministic finite automaton[zz/zz] 
# Now making triplet list[zz/zz]
# xx categories, zz nodes, bb arcs
# -> minimized: aa nodes, cc arcs
# ---
# generated: controller.dfa controller.term controller.dict

設定ファイルの作成

ひとつ上のディレクトリに戻り,以下に示す設定ファイルを作成する.ここでは,「controller.jconf」として保存した.

~/juliusKit/grammarKit/controller $ popd
~/juliusKit/grammarKit $ touch controller.jconf
~/juliusKit/grammarKit $ vim controller.jconf # エディタは自分の使いやすいものを利用する
# === ここから ===
######################################################################
#### 入出力指定
######################################################################
-charconv SJIS UTF8

######################################################################
#### ファイル指定
######################################################################
##
## DFA ファイル:文法から生成
## これは別途指定のこと(ヘッダの例を参照)
##
-dfa controller/controller.dfa

##
## 単語辞書ファイル
## これは別途指定のこと(ヘッダの例を参照)
##
-v controller/controller.dict

##
## gram の指定
##
-gram controller/controller

##
## 音響HMM定義ファイル
##
## triphoneモデル
# asciiフォーマット,もしくは "mkbinhmmで" 作成したバイナリ形式
# (自動判別される)
-h model/phone_m/hmmdefs_ptm_gid.binhmm   # PTM triphone
-hlist model/phone_m/logicalTri

######################################################################
#### 言語モデル詳細設定
######################################################################
##
## 単語挿入ペナルティを指定
##
#-penalty1 0.0     # 第1パス
#-penalty2 0.0     # 第2パス

######################################################################
#### 単語辞書詳細設定
######################################################################
##
## エラー単語を無視して続行する
##
-forcedict

######################################################################
#### 音響モデル詳細設定
######################################################################
##
## Julius が triphone/monophone の自動判別に失敗する場合,
## 以下を試してみてください.
##
#-no_ccd       # 音素環境依存性を(強制的に)考慮しない
#-force_ccd        #    〃      (強制的に)考慮する

##
## 特徴パラメータの型チェックをスキップしたい場合は,
## 以下を試してみてください.
##
#-notypecheck
#

##
## PTM/triphone 使用時,第1パスの単語間triphoneの音響尤度計算方法を指定する.
##
#-iwcd1 best N # 同コンテキストtriphoneの上位N個の平均値
#-iwcd1 max    # 同コンテキストtriphoneの最大値
-iwcd1 avg  # 同コンテキストtriphoneの平均値 (default)

######################################################################
#### Gaussian Pruning パラメータ(tied-mixture, PTMでのみ有効)
######################################################################
## コードブックあたり計算するガウス分布計算数(上位N個)
## 以下のデフォルト値は IPA99 の PTM モデル(1コードブックあたり64混合)
## に合わせた値
-tmix 2

## Gaussian pruning 法の選択
## 高速版ではbeam,それ以外ではsafeがデフォルトです
-gprune safe        # safe pruning 上位N個が確実に求まる.正確.
#-gprune heuristic # heuristic pruning
#-gprune beam      # beam pruning 次元ごとに足切り.高速.
#-gprune none      # pruning を行わない

######################################################################
#### Gaussian Mixture Selection パラメータ
######################################################################
#-gshmm hmmdefs        # GMS 用モノフォン音響モデルを指定
            # 指定なし = GMS OFF
#-gsnum 24     # GMS使用時の選択状態数

######################################################################
#### 探索パラメータ
######################################################################
#-b 400            # 第1パスのビーム幅(ノード数) monophone
-b 800         # 第1パスのビーム幅(ノード数) triphone,PTM
#-b 1000       # 第1パスのビーム幅(ノード数) triphone,PTM,engine=v2.1
-b2 30         # 第2パスの仮説数ビームの幅(仮説数)
-sb 80.0      # score beam envelope threshold
-s 5000            # 第2パスの最大スタック数 (仮説数)
-m 2000        # 第2パスの仮説オーバフローのしきい値
-lookuprange 5     # 第2パスで単語展開時のトレリス制約緩和幅(フレーム数)
-n 5           # 第2パスで見つける文の数(文数)
#-n 10         #   ('standard' 設定時のデフォルト)
-output 1      # 第2パスで見つかった文のうち出力する数 (文数)
#-looktrellis      # 単語仮説を完全にトレリス内の単語だけに絞る

######################################################################
#### 単語間ショートポーズ
######################################################################
##
## (マルチパス版のみ)
##
#-iwsp         # コンテキスト独立な単語間ショートポーズを付与
#-iwsppenalty 0.0  # ショートポーズへの遷移ペナルティ
#-spmodel "sp"       # ショートポーズ音響モデルの名前

######################################################################
#### 音声入力ソース
######################################################################
## どれかを選んでください(デフォルト:mfcfile)
#-input mfcfile        # HTK形式のパラメータファイル(MFCC)
#-input rawfile        # 音声波形データファイル(フォーマット自動判別)
            # 形式:WAV(16bit) または
            #  RAW(16bit(signed short),mono,big-endian)
            #  16kHz以外のファイルは -smpFreq で周波数指定
-input mic      # マイクから直接入力
#-input netaudio -NA host:0    # host上のDatLink(NetAudio)から入力
#-input adinnet -adport portnum # adinnet クライアントからの入力
#-input stdin      # 標準入力からの入力

#-filelist filename    # 認識対象ファイルのリスト

-nostrip        # ゼロ続きの無効な入力部の除去をOFFにする
            # (default: 無効な入力部分は除去される)
-zmean          # DC成分の除去を行う (-input mfcfile時無効)

######################################################################
#### 音声録音
######################################################################
#-record directory # 認識した音声データを連続したファイルに自動保存

######################################################################
#### 入力の棄却
######################################################################
-rejectshort 800   # 指定ミリ秒以下の長さの入力を棄却する

######################################################################
#### 音声区間検出
######################################################################
#-pausesegment     # レベル・零交差による音声区間検出の強制ON
#-nopausesegment   # レベル・零交差による音声区間検出の強制OFF
            # (default: mic または adinnet は ON, file は OFF)
-lv 3000       # レベルのしきい値 (0-32767)
#-headmargin 300   # 音声区間開始部のマージン(単位: msec)
#-tailmargin 400   # 音声区間終了部のマージン(単位: msec)
#-zc 60            # 1秒あたりの零交差数のしきい値

######################################################################
#### 音響分析
######################################################################
#-smpFreq 16000        # サンプリング周波数(Hz)
#-smpPeriod 625        # サンプリング周期(ns) (= 10000000 / smpFreq)
#-fsize 400        # 窓サイズ(サンプル数)
#-fshift 160       # フレームシフト幅(サンプル数)
#-delwin 2     # デルタウィンドウ幅 (フレーム数)
#-hifreq -1        # 高域カットオフの周波数(Hz) (-1: disable)
#-lofreq -1        # 低域カットオフの周波数(Hz) (-1: disable)
#-cmnsave filename # CMNパラメータをファイルに保存(1入力毎に上書き)
#-cmnload filename # 初期CMNパラメータを起動時ファイルから読み込む

######################################################################
#### スペクトルサブトラクション (SS)
######################################################################
#-sscalc       # 先頭の無音部を利用して SS を行う(ファイル入力のみ)
#-sscalclen 300        # SSに用いる先頭の無音部の長さ (msec)
#-ssload filename       # ファイルからノイズスペクトルを読み込む
#-ssalpha 2.0      # アルファ係数
#-ssfloor 0.5      # フロアリング係数

######################################################################
#### Forced alignment
######################################################################
#-walign       # 認識結果の単語ごとのアラインメント結果を出力
#-palign       # 認識結果の音素ごとのアラインメント結果を出力
#-salign       # 認識結果のHMM状態ごとのアラインメント結果を出力

######################################################################
#### 単語信頼度計算
######################################################################
#-cmalpha 0.05     # スムージング係数を指定

######################################################################
#### 出力スタイル
######################################################################
#-separatescore        # 言語スコアと音響スコアを分けて出力する
#-progout      # 第1パスで解析途中から漸次的に結果を出力
#-proginterval 300 # -progout 時の出力のインターバル(単位:msec)
-quiet          # 第1パス・第2パスの認識結果のみ出力
#-demo         # "-progout -quiet" と同じ
#-debug            # 探索中の内部状態を出力させる(デバッグ用)

######################################################################
#### サーバーモジュールモード
######################################################################
#-module       # サーバーモジュールモードで起動
#-module 5530      # (ポート番号を指定する場合)
#-outcode WLPSC        # モジュールに出力する情報を選択 (WLPSCwlps)
# === ここまで ===

動作確認

以下のディレクトリでコマンドを実行したい際に,「<<< please speak >>>」が出力され,音声が認識されていれば成功である.

~/juliusKit/grammarKit $ ALSADEV="plughw:0,0" julius -C controller.jconf -demo

WSF を用いた Windows メニューの操作

どうも,筆者です.

最近,Windows メニューの操作をする必要が出てきたので,その方法についてまとめておく.

Power Shell 等を利用した方が良いと思うが,時間がなかった為,知っている方法でまとめる.

<job id="chkUser">
    <script language="VBScript">
       ' InputBox
        function vbsInputBox(prompt)
            vbsInputBox = InputBox(prompt)
        end function
    </script>

    <script language="JScript">
        var winFS = WScript.CreateObject('Scripting.FileSystemObject');
        var winShell = WScript.CreateObject("WScript.Shell");
        // 待機時間
        var waitTime = 200;
        // 表示時間
        var displayTime = 3000;

        // InputBox の利用(VBScript の定義を呼び出す)
        function inputBox(prompt_) {
            return vbsInputBox(prompt_);
        }

        // ファイル名を指定して実行の起動
        function openExecutionWindow() {
            // 日本語を使うため、クリップボードにコピー
            winShell.Run("cmd.exe /c echo ファイル名を指定して実行 | clip", 0, true);
            winShell.SendKeys("^{ESC}");
            WScript.Sleep(waitTime);
            winShell.SendKeys("^v");
            winShell.SendKeys("{ENTER}");
            WScript.Sleep(waitTime);
        }

        // タブの移動
        function moveTab(count) {
            for (var i = 0; i < count; i++) {
                winShell.SendKeys("^{TAB}");
                WScript.Sleep(waitTime);
            }
        }

        // セキュリティウィンドウの表示
        function openSecurityWindow() {
            // === フォルダのプロパティメニューが開いている前提 ===
            // 1. 「セキュリティ」タブに移動
            moveTab(2);
            // 2. 「詳細設定」を押下
            winShell.SendKeys("%V");
            WScript.Sleep(waitTime);
            // 3. 「有効なアクセス許可」タブに移動
            moveTab(3);
        }

        // ユーザ名の確認
        function chkUserName(username) {
            // === 「有効なアクセス許可」タブが開いている前提 ===
            // 1. 「選択」を押下
            winShell.SendKeys("%S");
            WScript.Sleep(waitTime);
            // 2. 「ユーザ名」を入力
            winShell.SendKeys(username);
            WScript.Sleep(waitTime);
            // 3. 「名前の確認」を押下
            winShell.SendKeys("%C");
            WScript.Sleep(waitTime);
            // 4. 「OK」を押下
            winShell.SendKeys("{ENTER}");
            WScript.Sleep(waitTime);
        }

        // メイン処理
        var usePath = inputBox("対象フォルダまでの絶対パスを入力");
        var useUserNames = inputBox("ユーザ名の一覧を入力[要 セミコロン区切り]");
        // 引数確認
        if ((usePath != null) && winFS.FolderExists(usePath) && (useUserNames != null)) {
            var fullPath = winFS.GetAbsolutePathName(usePath);
            winShell.Run(fullPath);
            WScript.Sleep(waitTime);
            winShell.SendKeys("%{UP}");
            WScript.Sleep(waitTime);
            winShell.SendKeys("%{ENTER}");
            WScript.Sleep(waitTime);
            openSecurityWindow();

            var userList = useUserNames.replace(/;$/, "").split(";");
            for (var i = 0; i < userList.length; i++) {
                chkUserName(userList[i]);
                WScript.Sleep(displayTime);
            }
            WScript.Echo("Complete");
        }
    </script>
</job>

取り敢えず,プログラムだけ載せておく.

追記

【追記】2018 年 6 月 1 日 Power Shell 使えば楽なことに気付いた.

参考サイト

https://blogs.technet.microsoft.com/junichia/2009/12/02/scriptadfilereg-get-acl/

https://docs.microsoft.com/ja-jp/powershell/scripting/getting-started/cookbooks/creating-a-custom-input-box?view=powershell-6

gallery.technet.microsoft.com

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = 'アクセス権を調べる'
$form.Size = New-Object System.Drawing.Size(300,150)
$form.StartPosition = 'CenterScreen'

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(70,70)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = 'OK'
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $OKButton
$form.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Point(155,70)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = 'Cancel'
$CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $CancelButton
$form.Controls.Add($CancelButton)

$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = 'アクセス権を調査するフォルダの絶対パスを入力'
$form.Controls.Add($label)

$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(10,40)
$textBox.Size = New-Object System.Drawing.Size(260,20)
$form.Controls.Add($textBox)

# 最大化ボタンを無効化
$Form.MaximizeBox = $false
# 最小化ボタンを無効化
$Form.MinimizeBox = $false
# ウィンドウサイズを固定
$Form.FormBorderStyle = 'Fixed3D'
# 常に前面に表示
$form.Topmost = $true

$form.Add_Shown({$textBox.Select()})
$result = $form.ShowDialog()

# 出力先
$outputDir = [Environment]::GetFolderPath("Desktop")
# 出力ファイル名
$csvFile = $outputDir + "\accesslist.txt"

if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
    $filePath = $textBox.Text

    # 引数の確認
    if ($filePath) {
        # ファイルパスの確認
        if (Test-Path $filePath) {
            # アクセス権の取得
            $acl = Get-Acl -Path $filePath
            # アクセス権を持つユーザを出力
            $acl.Access | ForEach-Object { 
                $outArray = $_.FileSystemRights, $_.AccessControlType, $_.identityReference.value
                $outString = $outArray -join ","
                Write-Output $outString
            } | Out-File -FilePath $csvFile
        }
    }
}

bat ファイルでも作成できるから,ダブルクリックで起動可能.

参考サイト

reosablo.hatenablog.jp

@setlocal enabledelayedexpansion&set a=%*&(if defined a set a=!a:"=\"!&set a=!a:'=''!)&powershell/c $i=$input;iex ('$i^|^&{$PSCommandPath=\"%~f0\";$PSScriptRoot=\"%~dp0";#'+(${%~f0}^|Out-String)+'} '+('!a!'-replace'[$(),;@`{}]','`$0'))&exit/b

# Power Shell のスクリプト
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = 'アクセス権を調べる'
$form.Size = New-Object System.Drawing.Size(300,150)
$form.StartPosition = 'CenterScreen'

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(70,70)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = 'OK'
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $OKButton
$form.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Point(155,70)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = 'Cancel'
$CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $CancelButton
$form.Controls.Add($CancelButton)

$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.Text = 'アクセス権を調査するフォルダの絶対パスを入力'
$form.Controls.Add($label)

$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(10,40)
$textBox.Size = New-Object System.Drawing.Size(260,20)
$form.Controls.Add($textBox)

# 最大化ボタンを無効化
$Form.MaximizeBox = $false
# 最小化ボタンを無効化
$Form.MinimizeBox = $false
# ウィンドウサイズを固定
$Form.FormBorderStyle = 'Fixed3D'
# 常に前面に表示
$form.Topmost = $true

$form.Add_Shown({$textBox.Select()})
$result = $form.ShowDialog()

# 出力先
$outputDir = [Environment]::GetFolderPath('Desktop')
# 出力ファイル名
$csvFile = $outputDir + '\accesslist.txt'

if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
    $filePath = $textBox.Text

    # 引数の確認
    if ($filePath) {
        # ファイルパスの確認
        if (Test-Path $filePath) {
            # アクセス権の取得
            $acl = Get-Acl -Path $filePath
            # アクセス権を持つユーザを出力
            $acl.Access | ForEach-Object { 
                $outArray = $_.FileSystemRights, $_.AccessControlType, $_.identityReference.value
                $outString = $outArray -join ','
                Write-Output $outString
            } | Out-File -FilePath $csvFile
        }
    }
}

音声認識による赤外線機器の操作 その 3【赤外線操作編】

どうも,筆者です.

音声認識も終わり,やっと赤外線操作ができる.ただし,ここで説明するのは,ADRSIR に向けた解説になるため,他の機器を使っている人は別サイトを参考にしてほしい.

以下のサイトを参考にさせていただいた.

Amazon Echoから赤外線リモコン機器(テレビ、照明など)をコントロールする(ラズベリー・パイ専用 学習リモコン基板 ADRSIRを使って) - Qiita

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

workspacememory.hatenablog.com

前提

ここからは,プログラミングを行っていく.利用するライブラリが Python で書かれているため,Python を利用する.C 言語で開発しても良かったが,ライブラリを自分で作成するのは手間であるため,Python を利用することにした.

準備

まず,Raspberry Pi の電源を落とし,ADRSIR を GPIO ピンに接続する.利用するピンは決まっているため,他にも利用したいピンがある場合,ブレッドボードなどを用いて配線する. 筆者は,特に用途がなかった為,そのまま全部のピンを埋める形で接続した.

Raspberry Pi を起動後,以下のサイトから,「リモコンコードリスト(CSV)」をダウンロードする.また,有志の方が作成したライブラリを GitHub からダウンロードする.

ラズベリー・パイ専用学習リモコン基板

http://bit-trade-one.co.jp/support/download/

赤外線学習リモコン基板 ADRSIR 関連スクリプト

GitHub - you0708/adrsir: 赤外線学習リモコン基板 ADRSIR 関連スクリプト

# GitHub からダウンロードする
git clone https://github.com/you0708/adrsir.git

ダウンロードした「リモコンコードリスト(CSV)」は,adrsir に保存する.今後は,adrsir ディレクトリで作業する.

赤外線信号の作成

ADRSIR を利用して赤外線信号を登録する.ただ,筆者の環境の場合,テレビと電気は「リモコンコードリスト(CSV)」内にあったため,そのまま利用した.

ただ,どれがどれに対応するか分からなかったため,会社と対象機器の動作で grep をかけて,順番に試した.その手順を示す.ここでは,「ソニーのテレビ」を対象とする.また,「リモコンコードリスト(CSV)」は,「infrared_data_0171225.csv」という名前に変更して扱っている.

対象データの抽出

まず,ソニーのテレビに対し,対象機器の動作として,「電源」を抽出する.

~/adrsir  $ cat infrared_data_0171225.csv | grep "ソニー" | grep "デジタルテレビ" | grep "電源"
# === 出力結果 ===
# "ソニー","デジタルテレビ1","電源","..."
# "ソニー","デジタルテレビ2","電源","..."
# "ソニー","デジタルテレビ3","電源","..."

データをファイルへ書き込む

次に,抽出したリストから赤外線信号を取り出し,ファイルに保存する Shell Script を作成する.ここでのファイル名は createIrDataToFile.sh とした.

#!/bin/bash
# createIrDataToFile.sh

count=1
cat infrared_data_0171225.csv | grep "ソニー" | \
grep "デジタルテレビ" | grep "電源" | sed -e 's|"||g' | -e "s|,| |g" \
while read a b c d; do
    outName=machine${count}.dat
    echo ${b} "->" ${outName}
    echo ${d} > ${outName}
    count=$(expr ${count} \+ 1)
done

Python スクリプトの実行

後は,作成した Shell Script を実行し,生成されたファイルを順に Python スクリプトで実行する.以下に例を示す.

~/adrsir  $ chmod +x createIrDataToFile.sh
~/adrsir  $ ./createIrDataToFile.sh
~/adrsir  $ python3 ir_control.py send machine1.dat
~/adrsir  $ python3 ir_control.py send machine2.dat
~/adrsir  $ python3 ir_control.py send machine3.dat

筆者は実行も面倒だったため,createIrDataToFile.sh を exeIrData.sh として作成した.スクリプトの中身も以下のように変更した.

#!/bin/bash
# exeIrData.sh

corpName="ソニー"
machine="デジタルテレビ"
operation="電源"
outName=tmp.$$
cat infrared_data_0171225.csv | grep "${corpName}" | \
grep "${machine}" | grep "${operation}" | sed -e 's|"||g' | -e "s|,| |g" \
while read a b c d; do
    echo ${b} "->" ${outName}
    echo ${d} > ${outName}
    python3 ir_control.py send ${outName}
    sleep 3
done
rm -f ${outName}

JSON ファイルの生成

対象機器が分かれば,後は関連する信号をすべて出力すればよい.ただ,「ir_control.py」をそのまま利用するとファイル数が膨大になるため,JSON 形式でまとめることにした.

まず,「リモコンコードリスト(CSV)」から対象機器の信号一覧を取得する.ここでは,「ソニーのデジタルテレビ2」を利用する場合を考える.処理自体は難しくなく,grep コマンドで該当のものをすべて抽出すればよい.

cat infrared_data_0171225.csv | grep "ソニー" | grep "デジタルテレビ2" | sed -e 's|"||g' > ret.data

次に,抽出したデータをコマンドとして扱えるように名前を付ける.手間ではあるが,日本語をそのまま扱うのは後々辛そうであるため,ここで変換しておく.例えば,「電源」は「power」,「音量」は「volume」というように名前を付ける.

最後に,以下のような Python スクリプトを作成し,JSON 形式に変換する.ここで,スクリプト名は createJson.py とした.

スクリプト

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

# createJson.py

import json
import sys, os

def createDictData(filename):
    listData = []
    for line in open(filename):
        data = line.replace('\n', '').split(',')
        listData.append((data[2], data[3]))
    return dict(listData)

if __name__ == "__main__":
    argv = sys.argv
    argc = len(argv)

    if argc < 2:
        print('Usage: {0} [infra red data file]'.format(argv[0]))
    else:
        filename = argv[1]
        rootName, _ = os.path.splitext(filename)
        dicData = createDictData(filename)
        with open(rootName + '.json', 'w') as fout:
            json.dump(dicData, fout)

変換方法

python3 createJson.py ret.data # ret.json が生成される

この JSON ファイルは,名前(power や volume)をキー(key)とし,赤外線信号を値(val)として持つ.Python で読み込めば,連想配列として利用できる.

赤外線信号の送信

JSON 形式で保存したファイルを用いて,赤外線を送信する.そのための Python スクリプトを以下に示す.ファイル名は irControl.py とした.

スクリプト

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

# irControl.py

import adrsirlib
import sys, json

# ===============
# display command
# ===============
def display_list(jsonFile):
    try:
        with open(jsonFile, 'r') as fin:
            dictData = json.load(fin)
            print("print json file data")
            listData = [key for key, _ in dictData.items()]

            for i, key in enumerate(sorted(listData)):
                print("No.{0:03d}: {1}".format(i, key))
    except json.JSONDecodeError as e:
        print('JSONDecodeError: ', e)

# =====
# Usage
# =====
def usage():
    print("\nUsage: {0} <send|list> [json file] [options...]\n".format(__file__))
    print("  command")
    print("    send : send data to ADRSIR")
    print("        ex) {0} send [json file] [send command]".format(__file__))
    print("    list : print json file data")
    print("        ex) {0} list [json file] all".format(__file__))

# ===========
# execute cmd
# ===========
def executeCmd(jsonFile, cmd):
    retVal = 1

    try:
        with open(jsonFile, 'r') as fin:
            jsonData = json.load(fin)

            try:
                adrsirlib.write(jsonData[sendCmd])
                retVal = 0
            except:
                print('command not found')
    except json.JSONDecodeError as e:
        print('JSONDecodeError: ', e)

if __name__ == '__main__':
    argv = sys.argv
    argc = len(argv)

    if argc < 4:
        usage()
    else:
        command = argv[1]
        jsonFile = argv[2]

        if command == 'send':
            sendCmd = argv[3]
            executeCmd(jsonFile, sendCmd)

        elif command == 'list':
            display_list(jsonFile)
        else:
            usage()

実行方法

python3 irControl.py list ret.json all # コマンドの一覧表示
python3 irControl.py send ret.json power # 電源ボタンの赤外線信号を送信

以上で,赤外線機器の操作ができるようになった.後は,音声認識結果と組み合わせればよい.次回以降で,組み合わせ方を解説する.

音声認識による赤外線機器の操作 その 2【辞書作成編】

どうも,筆者です.

前回に引き続き,今回は,Julius で認識用の辞書を作成する.前回までの記事は以下にある.

workspacememory.hatenablog.com

また,以下のサイトを参考にさせていただいた.

Raspberry pi上の音声認識(julius)認識率向上[julius辞書作成] - Qiita

辞書の作成

ここでは,辞書の作成方法について説明する.ただし,単語との 1 対 1 対応がとれる辞書を作成するため,語彙が増えた場合追いつかなくなると思われる. 現状では,認識対象の単語は,さほど数がないため問題ないが,いずれ文法ファイル等を用いた方法に変更する予定である.

今回は,以下の単語を Julius で認識することを考える.表現が微妙に変えてあるのは単語レベルで認識しやすくするためである.

電気つけて
電気オフ
明るくして
暗くして
こだまにして
テレビオン
テレビ切って

単語ファイルの作成

上記のリストを Julius が認識できる形式にするため,単語と読み方の対応を記述する必要がある.これも Julius の追加キットにツールがあるため,ありがたく利用させてもらう.

まずは,作業用ディレクトリに移動する.

~ $ pushd juliusKit

以下のような単語と読み方のファイルを作成し,「word.yomi」として保存する.形式は「単語[タブ]読み方」となる.

電気つけて  でんきつけて
電気オフ    でんきおふ
明るくして あかるくして
暗くして    くらくして
こだまにして  こだまにして
テレビオン てれびおん
テレビ切って  てれびきって

この読み方をローマ字形式に変換する.そのための Shell Script を以下に示す(ファイル名は outYomi.sh とした).

#!/bin/bash
# outYomi.sh

if [ $# -lt 1 ]; then
    echo Usage: $0 "[yomi file]"
    echo "ex) $0 word.yomi"
    exit 1
fi

currentDir=${PWD}
yomiFile=$1
grammarDir=${currentDir}/grammarKit/bin/linux
dictationDir=${currentDir}/dictationKit_v4.3.1
cat ${yomiFile} | iconv -f utf8 -t eucjp | perl ${grammarDir}/yomi2voca.pl > ${dictationDir}/${yomiFile%.*}.dic

ここまでで,juliusKit のディレクトリ構造は以下のようになる.

~/juliusKit
   |--dictationKit_v4.3.1
   |--grammarKit
   |--outYomi.sh
   |--word.yomi

作成した「word.yomi」と「outYomi.sh」を用いて結果を出力する.出力結果は「dictationKit_v4.3.1」以下に出力される.

~/juliusKit $ chmod +x outYomi.sh
~/juliusKit $ ./outYomi.sh word.yomi # dictationKit_v4.3.1/word.dic として保存される

実行結果は以下のようになる.ただ,ファイルは UTF-8 でないため,Linux の場合,そのまま見ると文字化けする.

電気つけて  d e N k i ts u k e t e
電気オフ    d e N k i o f u
明るくして a k a r u k u sh i t e
暗くして    k u r a k u sh i t e
こだまにして  k o d a m a n i sh i t e
テレビオン t e r e b i o N
テレビ切って  t e r e b i k i q t e

Julius 設定ファイルの作成

次に,Julius の設定ファイルを作成する.この辺りから詳しく分かっていない.サンプルを見ながら編集したものとなる.

dictationKit_v4.3.1 以下に「word.jconf」というファイルを作成し,以下の内容を記述する.

~/juliusKit $ pushd dictationKit_v4.3.1
~/juliusKit/dictationKit_v4.3.1 $ touch word.jconf
~/juliusKit/dictationKit_v4.3.1 $ vim word.jconf # エディタは自分の使いやすいものを利用する
# === ここから ===
-w word.dic                               # 単語辞書ファイル
-v model/lang_m/bccwj.60k.htkdic          # N-gram または文法用の単語辞書ファイルを指定
-h model/phone_m/jnas-tri-3k16-gid.binhmm # 使用する HMM 定義ファイル
-hlist model/phone_m/logicalTri           # HMMlist ファイルを指定する
-b 1500                                   # 第 1 パスのビーム幅(ノード数)
-b2 100                                   # 第 2 パスの仮説数ビームの幅(仮説数)
-s 500                                    # 第 2 パスの最大スタック数(仮説数)
-m 10000                                  # 第 2 パスの仮説オーバフローのしきい値
-n 5                                      # n 個の文仮説数が見つかるまで検索を行う
-output 1                                 # 見つかった N-best 候補のうち、結果として出力する個数
-input mic                                # マイク使用
-zmeanframe                               # フレーム単位のDC成分除去を行う(HTKと同処理)
-rejectshort 800                          # 検出された入力が指定時間(msec)以下なら棄却
-lv 3000                                  # 検出された入力の振幅の閾値。範囲内のみの音声を解析対象とする
-charconv EUC-JP UTF-8                    # 入出力エンコード指定(内部:euc-jp、出力:utf-8)
# === ここまで ===

注意することとして,「.jconf」ファイルに記述してあるパスは,「.jconf」があるディレクトリをカレントディレクトリとし,ここからの相対パスで記述されている.そのため,このファイルを移動する場合,パスも一緒に移動するか,対象の定義ファイルをコピーしておく必要がある.

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

~/juliusKit
   |--dictationKit_v4.3.1
       |--word.dic
       |--word.jconf
   |--grammarKit
   |--outYomi.sh
   |--word.yomi

動作確認

作成した「word.jconf」で動作確認を行う.以下のコマンドを実行し音声を入力すると認識結果が表示される.登録した単語の何れかが表示されるはずである.

~/juliusKit/dictationKit_v4.3.1 $ ALSADEV="plughw:0,0" julius -C word.jconf -demo
# もしくは,~/juliusKit まで戻って,以下を実行
# ALSADEV="plughw:0,0" julius -C ./dictationKit_v4.3.1/word.jconf -demo

余談

気付いたら,「ALSADEV="plughw:0,0"」を指定しなくても動作するようになっていた.これは,途中で再起動したため,設定ファイルがリロードされたためだと思われる.