[音声認識] Kaldiによるspeech recognition その1

Github: kaldi
Homepage: KALDI

# What is Kaldi?
Kaldi is speech recognition toolkit written in C++
Ethiopian goatherder who discovered the coffee plant named Kaldi
Code level integration with Finite State Transducers
Extensive linear algebra support

# Downloading Kaldi
$ git clone https://github.com/kaldi-asr/kaldi.git kaldi –origin upstream
$ cd kaldi
INSTALL

Option 1 (bash + makefile):
  Steps:
    (1)
    go to tools/  and follow INSTALL instructions there.
    (2)
    go to src/ and follow INSTALL instructions there.

$ cd tools
$ extras/check_dependencies.sh
$ sudo apt-get install automake autoconf unzip sox gfortran libtool subversion python2.7
$ make

libtool: compile: g++ -DHAVE_CONFIG_H -I./../include -fno-exceptions -funsigned-char -g -O2 -std=c++11 -MT fst-types.lo -MD -MP -MF .deps/fst-types.Tpo -c fst-types.cc -fPIC -DPIC -o .libs/fst-types.o

ここで止まる
何でやねん

mklが入ってなかったようなので、もう一度やります
$ extras/check_dependencies.sh
$ extras/install_mkl.sh
$ make

g++: fatal error: Killed signal terminated program cc1plus
compilation terminated.
make[3]: *** [Makefile:460: fst-types.lo] Error 1
make[3]: Leaving directory ‘/home/vagrant/kaldi/kaldi/tools/openfst-1.7.2/src/lib’
make[2]: *** [Makefile:370: install-recursive] Error 1
make[2]: Leaving directory ‘/home/vagrant/kaldi/kaldi/tools/openfst-1.7.2/src’
make[1]: *** [Makefile:426: install-recursive] Error 1
make[1]: Leaving directory ‘/home/vagrant/kaldi/kaldi/tools/openfst-1.7.2’
make: *** [Makefile:64: openfst_compiled] Error 2

メモリが足りない時のエラーみたい

$ nproc
2
4GBにして、swapを増やし、CPU二つで並列でmakeしたい

[音声認識] DeepSpeechを試そう

DeepSeechとは?
– DeepSpeech is an open-source Speech to Text engine, trained by machine learning based on Baidu’s Deep Speech research paper and using TensorFlow.

DeepSpeech Document: deepspeech.readthedocs.io.

# create a virtualenv
$ sudo apt install python3-virtualenv
$ source deepspeech-venv/bin/activate

# install DeepSpeech
$ pip3 install deepspeech

# download pre-trained English model
$ curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/deepspeech-0.9.3-models.pbmm
$ curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/deepspeech-0.9.3-models.scorer

# Download example audio files
$ curl -LO https://github.com/mozilla/DeepSpeech/releases/download/v0.9.3/audio-0.9.3.tar.gz
$ tar xvf audio-0.9.3.tar.gz

$ deepspeech –model deepspeech-0.9.3-models.pbmm –scorer deepspeech-0.9.3-models.scorer –audio audio/2830-3980-0043.wav
Loading model from file deepspeech-0.9.3-models.pbmm
TensorFlow: v2.3.0-6-g23ad988
DeepSpeech: v0.9.3-0-gf2e9c85
2021-08-24 22:27:18.338821: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN)to use the following CPU instructions in performance-critical operations: AVX2
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
Loaded model in 0.0447s.
Loading scorer from files deepspeech-0.9.3-models.scorer
Loaded scorer in 0.00898s.
Running inference.
experience proves this
Inference took 2.371s for 1.975s audio file.

なるほど、Juliusと似ているところがあるね
.wavファイルを作成せずにmicrophoneでrealtime speech recognitionを作りたいな。

[音声認識] pythonでjuliusを読み込んでテキスト出力

$ ../julius/julius/julius -C julius.jconf -dnnconf dnn.jconf -module

モジュールモードで起動する

app.py

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

import socket
import time

HOST = '192.168.33.10'
PORT = 10500
DATASIZE = 1024

class Julius:

	def __init__(self):
		self.sock = None  # constructor

	def run(self):

		with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as self.sock:
			self.sock.connect((HOST, PORT))

			text = ""
			fin_flag = False

			while True: # 無限ループ

				data = self.sock.recv(DATASIZE).decode('utf-8')

				for line in data.split('\n'):
					index = line.find('WORD="')
					if index != -1:
						rcg_text = line[index+6:line.find('"',index+6)]
						stp = ['<s>', '</s>']
						if(rcg_text not in stp):
							text = text + ' ' + rcg_text

					if '</RECOGOUT>' in line: # </RECOGOUT>で1sentence終わり
						fin_flag = True

				if fin_flag == True:
					print(text)

					fin_flag = False
					text = ""

if __name__ == "__main__": # importしても実行されない

	julius = Julius()
	julius.run()

$ python3 app.py
plans are well underway already martin nineteen ninety two five dollars bail
director martin to commemorate kilometer journey to the new world five hundred years ago and wanted moving it to promote use of those detailed in exploration

ほう、なるほど
printではなくファイルに保存であれば、会議の議事録をメールで送るなども割と簡単に実装できそうやな。

さて資料作るか🥺

[音声認識] JuliusでError: adin_file: channel num != 1 (2)

$ ../julius/julius/julius -C julius.jconf -dnnconf dnn.jconf

### read waveform input
Error: adin_file: channel num != 1 (2)
Error: adin_file: error in parsing wav header at mozilla.wav
Error: adin_file: failed to read speech data: “mozilla.wav”
0 files processed

チャンネル数が1chでない=ステレオ のエラーに様です。
WAVファイルはWindows標準の音データファイルでRIFF形式で作られている。
RIFFにはchunkと呼ばれる考え方があり、wavはいくつかのチャンクを1つにまとめた集合体
識別子(4), Size(4), Data(n)

### モノラルとステレオ
モノラルというのは、左右から違う音が聴こえる音声
ステレオとは違い真ん中からしか聴こえない音声のこと

Soxをインストールします
$ sudo git clone git://sox.git.sourceforge.net/gitroot/sox/sox
$ cd sox
$ sudo yum groupinstall “Development Tools”
$ ./configure
-bash: ./configure: No such file or directory

$ yum install sox

$ sox mozilla.wav -c 1 test.wav
$ ../julius/julius/julius -C julius.jconf -dnnconf dnn.jconf
Error: adin_file: sampling rate != 16000 (44100)
Error: adin_file: error in parsing wav header at mozilla.wav
Error: adin_file: failed to read speech data: “mozilla.wav”

$ sox mozilla.wav -c 1 -r 16000 test1.wav
——
### read waveform input
Stat: adin_file: input speechfile: mozilla.wav
STAT: 0 samples (0.00 sec.)
STAT: ### speech analysis (waveform -> MFCC)
WARNING: input too short (0 samples), ignored

ファイルを変えて再度やります。
id: from to n_score unit
—————————————-
[ 0 2] -0.890920 []
[ 3 43] 1.508327 plans [plans]
[ 44 52] 0.579483 are [are]
[ 53 83] 2.098300 well [well]
[ 84 141] 1.983006 underway [underway]
[ 142 219] 1.388610 already [already]
[ 220 309] 1.076294 martin [martin]
[ 310 364] 1.698448 nineteen [nineteen]
[ 365 398] 2.135265 ninety [ninety]
[ 399 472] 1.064299 two [two]
[ 473 504] 1.476521 five [five]
[ 505 561] 0.660421 dollars [dollars]
[ 562 608] 2.348794 bail [bail]
[ 609 736] 0.248682
[
]
re-computed AM score: 920.427368
=== end forced alignment ===

=== begin forced alignment ===
— word alignment —
id: from to n_score unit
—————————————-
[ 0 71] 0.859664 []
[ 72 111] 1.162892 director [director]
[ 112 164] 1.981413 martin [martin]
[ 165 180] 1.593118 to [to]
[ 181 221] 2.427887 commemorate [commemorate]
[ 222 267] 1.872279 kilometer [kilometer]
[ 268 306] 2.526583 journey [journey]
[ 307 319] 2.079670 to [to]
[ 320 327] 2.000595 the [the]
[ 328 348] 3.200890 new [new]
[ 349 386] 2.590411 world [world]
[ 387 414] 2.556754 five [five]
[ 415 443] 1.544829 hundred [hundred]
[ 444 464] 0.974130 years [years]
[ 465 531] 1.067814 ago [ago]
[ 532 546] 1.595085 and [and]
[ 547 583] 1.752286 wanted [wanted]
[ 584 642] 1.655993 moving [moving]
[ 643 658] 2.205574 it [it]
[ 659 670] 2.086497 to [to]
[ 671 704] 2.005465 promote [promote]
[ 705 732] 1.775316 use [use]
[ 733 755] 1.450466 of [of]
[ 756 773] 1.704210 those [those]
[ 774 856] 1.187828 detailed [detailed]
[ 857 887] 1.474861 in [in]
[ 888 990] 2.152141 exploration [exploration]
[ 991 1010] 0.570776
[
]
re-computed AM score: 1743.703125
=== end forced alignment ===

精度には問題があるが、一連の流れとしてはOKかな。

[音声認識] JuliusでError: adin_oss: failed to open /dev/dsp

vagrant ec2にjuliusをダウンロードしてmacのマイクから音声認識をしようとしたところ、

$ ../julius/julius/julius -C mic.jconf -dnnconf dnn.jconf
### read waveform input
Stat: adin_oss: device name = /dev/dsp (application default)
Error: adin_oss: failed to open /dev/dsp
failed to begin input stream

どうやらsnd-pcm-ossで回避できるらしい。
$ sudo yum install osspd-alsa
329 packages excluded due to repository priority protections
????? osspd-alsa ?????????
エラー: 何もしません

別の方法を考えるか。

[音声認識] Juliusの認識アルゴリズム

### 入力音声の認識アルゴリズム
特徴量系列に対して、音響モデルと言語モデルの元で、確率が最大となる単語列を見つけ出す
ツリートレスト探索方式を基礎とするアルゴリズム
第一パスと第二パスの段階によって絞り込む
 L 単語履歴の 1-best近似, N-gram における 1-gram factoring, 部分線形化辞書, 単語間トライフォン近似を用いる
L 単語候補集合の算出

探索アルゴリズムによる調節可能なパラメータ
認識処理インスタンスごとに設定して精度を調整する
  解探索を行う際の仮説の足切り幅,すなわちビーム幅を設定できる

### 認識結果の出力
N-bestリスト
L 指定された数の文仮説数が見付かるまで探索を行う
単語ラティス形式
L 認識結果の上位仮説集合を,単語グラフ(ラティス)形式で出力できる
Confusion network
L 認識結果を confusion network の形で出力

### 複数モデルを用いた認識
入力に対して並列に認識を行い, 複数の結果を一度に得ることが可能
– 音響モデルインスタンの宣言(-AM)
– 言語モデルインスタンス(-LM)
– 認識処理インスタンス(-SR)
各指定のオプションをjconfファイルに記載する

### モジュールモード
Juliusをモジュールモードで起動することで、音声認識サーバとして動かすことができる。
起動後、クライアントからのTCP/IP接続待ちとなる
ADDGRAM, CHANGEGRAM では,クライアントから Julius へ文法を送信

### プラグイン
– 音声入力、音声後処理、特徴量入力、特徴量後処理、ガウス分布計算、結果取得、初期化・処理開始など

全体像については何となく理解できました。

[音声認識] Juliusの音響モデルと言語モデル

### 音響モデル
音響モデルとしてHMM(Hidden Markov Model)を用いることが出る。
コンテキスト非依存モデル、コンテキスト依存モデルまで扱える
出力確率モデルはガウス混合分布を基本として、tied-mixtureやphonetic tied-mixtureモデルを扱える

### 音響モデルのファイル形式
– HTK ascii形式
 L ASCII形式のHMM定義ファイルを読み込む
– Julius用バイナリ形式
L Julius用バイナリ形式へ変換しておくことで、読み込み時間を短縮できる
  L 特徴量抽出の条件パラメータを埋め込むこともできる
– HMMListファイル
L 単語辞書の音素表記、トライフォンの論理音素名、実際の音響モデル上の物理音素名との対応を与えることができる

### 音素コンテキスト依存モデル
与えられた音韻モデルが音素環境依存モデルであるかどうかをHMMの名称パターンから判定する(logical phoneとphysical phone)
単語間トライフォン近似とは、単語の前後に接続する読みの影響を受けて変化すること

状態遷移とマルチパスモード
L 音響モデル内の状態遷移のパターンを調べ、そのパターンに対応する効率的な計算処理を選択する

### 言語モデル
単語N-gram、記述文法、単語リストなどの言語モデルをサポートしている

– 単語辞書
L 認識対象とする単語とその読みを定義する
L ファイル形式はHTKの辞書フォーマットを拡張したもので第一フィールは言語制約の対応エントリ
  L 第二フィールはエントリ内確率、第三フィールドは出力文字列、以降は音素列
  L 情報が少ない場合は透過単語として指定する、無音用単語の追加もできる

– N-gram
  L N-gramファイルおよび各単語の読みを記述した単語辞書の2つをJuliusに与えて認識する
  L ARPA標準形式とJuliusバイナリ形式をサポートしている
  L SRIMでは、文開式記号について特殊な処理が行われる

デフォルトの語彙数は65534語である
与えられた文法上で可能な文のパターンから、入力に最もマッチする文候補が選ばれ、認識結果として出力される
  L grammarファイルとvocaファイルがある
L grammarファイルでは単語のカテゴリ間の構文制約をBNF風に記述する
  L vocaファイルはgrammarファイルで記述した終端記号ごとに単語を登録する ”%”で始まる行はカテゴリ定義

grammarファイルとvocaファイルをmkdfa.plを用いてdfaファイルとdictファイルに変換する

文法における文中の短時間無音の指定
 L 息継ぎによる発声休止を指定することができる

DFAファイルはオートマトン定義ファイルであり、文法制約を有限状態のオートマントンに変換している

– 単語リスト
音声コマンドや音声リモコンのような単純で簡単な音声認識を手軽に実現できるよう孤立単語認識を行える

—-
なるほど、grammarファイルとvocaファイルを作成してコンパイルするのね。
発音音素列の記述は英字で書くのか。
音声から文章にする場合は、形態素分析のように品詞の指定は行ってないのね。
ARPA標準フォーマットがどういう形式なのか気になるな。

[音声認識] Juliusの音声データ入力

### 基本フォーマット
– 量子化ビット数は16ビット固定
– チャンネル数は1チャンネル
– 入力のサンプリングレート(Hz)は、オプションで指定。デフォルトは16,000Hz
– .wavファイル、ヘッダ無しRAWファイルを読み込む
– 録音デバイス(16bit)からの直接入力
– ネットワーク・ソケット経由や特徴量ファイルの入力もできる

### フロントエンド処理
– 音声特徴量は、短時間ごとに切り出された音声信号から抽出される特徴ベクトルの時系列で、特徴抽出後、認識処理(解探索)を行う
– 特徴抽出の前処理にフロントエンド処理が実装されている
– 入力音声波形に対する信号処理

■直流成分除去
直流成分であるオフセット値の推定方法
 L 短時間音声区間ごとに行う方法と、長時間平均が用意されている

■スペクトルサブトラクション
雑音のスペクトルを推定して音声信号から減算することで雑音の影響を抑圧する(ファン音など定常雑音の除去)

### 特徴量抽出
メル周波数ケプストラム係数(MFCC)および派生パラメータを抽出できる
L 対数ケプストラムの低次成分に対して、ヒトの周波数知覚特性を考慮した重み付けをした特徴量を、メル周波数ケプストラム係数(MFCC)と呼ぶ

### 正規化処理
環境や話者の影響を軽減するため、算出後の特徴量に対して正規化処理を行うことができる
– CMN、CVN、周波数ワーピング

### 音声区間検出・入力棄却
音声が発話された区間を検出する音声区間検出(Voice activity detection)
短時間ごとに音声区間の開始終了を検出し、それを元に認識単位の切り出しおよび発話単位の区切りを行う
L 零交差数が一定数を超えた時に、音声始端として認識処理を行う
  L ガウス混合分布モデルによって、音声と非音声のGMMを定義し、開始終了を判別する方法もある
入力処理の終了後に、事後的に入力を棄却することもできる

音声の区間を検出して、MFCCで特徴量を抽出してるのね。なるほど、仕組みがわかるとちょっと考え方が変わるね。

[音声認識] What is Julius?

### Julius
Juliusとは?
L オープンソースの高性能な汎用大語彙連続音声認識エンジン
L 数万語彙の連続認識を実時間で実行できる
L 単語N-gram、記述文法、単語辞書を用いることができる
  L 音響モデルとしてトライフォンのGMM-HMM、DNN-HMMを用いたリアルタイム認識を行うことができる
  L 単語辞書や言語モデル、音響モデルなど、音声認識の各モジュールを組み替えることで、様々な用途に応用できる

※N-gramとは、連続するn個の単語や文字のまとまり
※GMM-HMMとは隠れマルコフモデルで、モデルを固定すると(与えられると)、そのモデルからどのような時系列データが生成されやすいのか、の確率を与える
※DNN-HMMとは、GMMを使ってガウス分布の確率値として求めるのではなく, DNNの事後確率を使って、間接的に(遠回りして)求める。

### Juliusを使うには
– 音響モデル(音素HMM) … 音声波形パターン
– 単語辞書 … 単語の読みを定義
– 言語モデル(単語N-gram) … どのような単語列が出しやすいか、単語間の接続制約を決定
– 動作環境: linux, mac, windows

### 処理フロー
– 音声入力部、特徴抽出部、認識処理部
 L リアルタイム認識の場合は並列処理される
– 第二パスでは、単語トレリスと呼ばれる仮説集合を参照しながら、入力全体に対して再認識を行う

mecabのmecab-ipadic-NEologdが、単語辞書に当たるのか。
単語辞書や単語N-gramを独自開発するのは難しいから、既存のものを使用して開発するのか。。これもC言語で書かれているのだろうか?
特徴を抽出して再認識か googleのspeech recognitionも同じかわからんが、凄いね

[Laravel8.x] stancl/tenancyでマルチテナントを構築する手順

$ composer create-project –prefer-dist laravel/laravel stancl
$ cd stancl
$ composer require laravel/jetstream
$ php artisan jetstream:install livewire
$ composer require stancl/tenancy
$ php artisan tenancy:install

$ php artisan migrate
mysql> show tables;
+————————+
| Tables_in_stancl |
+————————+
| domains |
| failed_jobs |
| migrations |
| password_resets |
| personal_access_tokens |
| sessions |
| tenants |
| users |
+————————+
8 rows in set (0.01 sec)

config/app.php

App\Providers\TenancyServiceProvider::class,

app/Models/Tenant.php

use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;

class Tenant extends BaseTenant implements TenantWithDatabase {
	use HasDatabase, HasDomains;
}

config/tenancy.php

    'tenant_model' => \App\Models\Tenant::class,

### Central routes
app/Providers/RouteServiceProvider.php

    protected function mapWebRoutes(){
        foreach($this->centralDomains() as $domain){
            Route::middleware('web')
                ->domain($domain)
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        }
    }

    protected function mapApiRoutes(){
        foreach($this->centralDomains() as $domain){
            Route::prefix('api')
                ->domain($domain)
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));
        }
    }

    protected function centralDomains(): array {
        return config('tenancy.central_domains');
    }

    public function boot()
    {
        $this->configureRateLimiting();

        // $this->routes(function () {
        //     Route::prefix('api')
        //         ->middleware('api')
        //         ->namespace($this->namespace)
        //         ->group(base_path('routes/api.php'));

        //     Route::middleware('web')
        //         ->namespace($this->namespace)
        //         ->group(base_path('routes/web.php'));
        // });
        $this->mapWebRoutes();
        $this->mapApiRoutes();
    }

config/tenancy.php

    'central_domains' => [
        // '127.0.0.1',
        '192.168.33.10',
        // 'localhost',

    ],

routes/tenant.php

    Route::get('/', function () {
    	dd(\App\Models\User::all());
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });

move the users table migration (the file database/migrations/2014_10_12_000000_create_users_table.php or similar) to database/migrations/tenant.

php artisan tinker
>>> $tenant1->domains()->create([‘domain’ => ‘hoge.192.168.33.10’]);
=> Stancl\Tenancy\Database\Models\Domain {#4734
domain: “hoge.192.168.33.10”,
tenant_id: “hoge”,
updated_at: “2021-08-08 11:59:44”,
created_at: “2021-08-08 11:59:44”,
id: 1,
tenant: App\Models\Tenant {#4792
id: “hoge”,
created_at: “2021-08-08 11:59:35”,
updated_at: “2021-08-08 11:59:35”,
data: null,
tenancy_db_name: “tenanthoge”,
},
}
>>> $tenant2 = App\Models\Tenant::create([‘id’ => ‘bar’]);
=> App\Models\Tenant {#4794
id: “bar”,
data: null,
updated_at: “2021-08-08 11:59:50”,
tenancy_db_name: “tenantbar”,
}
>>> $tenant2->domains()->create([‘domain’ => ‘bar.192.168.33.10’]);
=> Stancl\Tenancy\Database\Models\Domain {#4786
domain: “bar.192.168.33.10”,
tenant_id: “bar”,
updated_at: “2021-08-08 11:59:55”,
created_at: “2021-08-08 11:59:55”,
id: 2,
tenant: App\Models\Tenant {#3788
id: “bar”,
created_at: “2021-08-08 11:59:50”,
updated_at: “2021-08-08 11:59:50”,
data: null,
tenancy_db_name: “tenantbar”,
},
}

$ php artisan make:seeder TenantTableSeeder

    public function run()
    {
        App\Tenant::all()->runForEach(function () {
		    factory(App\User::class)->create();
		});
    }

mysql> select * from tenants;
+——+———————+———————+———————————–+
| id | created_at | updated_at | data |
+——+———————+———————+———————————–+
| bar | 2021-08-08 11:59:50 | 2021-08-08 11:59:50 | {“tenancy_db_name”: “tenantbar”} |
| foo | 2021-08-08 11:53:06 | 2021-08-08 11:53:06 | {“tenancy_db_name”: “tenantfoo”} |
| hoge | 2021-08-08 11:59:35 | 2021-08-08 11:59:35 | {“tenancy_db_name”: “tenanthoge”} |
+——+———————+———————+———————————–+

流れはわかったが、名前解決が出来ないな。。。
これも、ドメインでやるのかな。

1. お名前.comでドメインを取得してVPSにデプロイします。
2. お名前.com側ではDNS側でAレコードをワイルドカードで設定、VPS側ではCNAMEを設定して再度試します。

できたーーーーーーーーーーーーーーーーーーああああああああああああ
ウヒョーーーーーーーーー

database/tenant/* の中に、テナント用のmigration fileを作るわけね。
マルチテナントの開発の場合は、localhostや192.168.33.10などは名前解決できないので、ドメインを取得してテストする必要がある。

うむ、なかなか大変だわこれ。