音声認識可能な対話型AIを作成してみた(VOSK, GiNZA, python,チャットボット)

こんにちにゃんです。
水色桜（みずいろさくら）です。
今回はVOSKというライブラリを用いて声を文字起こしし、それに対して自然な返答を行うようなAIを作っていきたいと思います。
まず、実行の様子を示します。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/vosk?src=hash&amp;ref_src=twsrc%5Etfw">#vosk</a> というライブラリを用いて音声認識を行い、<a href="https://twitter.com/hashtag/GiNZA?src=hash&amp;ref_src=twsrc%5Etfw">#GiNZA</a> で返答を生成するプログラムを作ってみました。<br>ゆっくり喋ればちゃんと会話ができます。<a href="https://twitter.com/hashtag/%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E5%87%A6%E7%90%86?src=hash&amp;ref_src=twsrc%5Etfw">#自然言語処理</a><a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング</a><a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%88%9D%E5%AD%A6%E8%80%85%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング初学者と繋がりたい</a> <a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a><a href="https://twitter.com/hashtag/%E7%94%B7%E3%81%AE%E5%A8%98?src=hash&amp;ref_src=twsrc%5Etfw">#男の娘</a><a href="https://twitter.com/hashtag/femboy?src=hash&amp;ref_src=twsrc%5Etfw">#femboy</a> <a href="https://t.co/gV9ChK7sPR">pic.twitter.com/gV9ChK7sPR</a></p>&mdash; 水色桜 (@Mizuiro__sakura) <a href="https://twitter.com/Mizuiro__sakura/status/1573559955592216579?ref_src=twsrc%5Etfw">September 24, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
このようにゆっくり話すとしっかり認識して、返答を返してくれます。音声認識はVOSK、返答の生成はGiNZAを用いて行っています。

まず、それぞれのライブラリについて説明します。
VOSKはalphacepheiさんが開発した２０言語以上に対応した音声認識ツールキットです。言語モデルが約50MBと軽く、組み込みを容易に行うことが可能です。

VOSK Offline Speech Recognition API
https://alphacephei.com/vosk/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/98848659-d412-8b59-7a11-dc3c83bb8aee.png)

VOSKのインストールは

```
pip install vosk
```

で行うことができます。またalphacepheiさんのWebページから日本語のモデル（https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip
）をダウンロードすることで日本語で音声認識をすることができます。zipファイルは展開して「vosk_model」に名前を変更してください。そしてpythonのプログラムと同じディレクトリにおいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847349/9b1eab35-56ff-19ea-cf4d-eb8fa791a273.png)

GiNZAはリクルートさんと国立国語研究所さんの開発した自然言語処理ライブラリです。GiNZAはspaCyというライブラリを元に作られています。spaCyはExplosion社さんが提供している自然言語処理ライブラリで、アメリカなどでは人気のあるライブラリです。製品利用を念頭に尽きられているためとっても使い勝手が良いのですが、なんと２０１９年までは日本語には対応していませんでした。そこでリクルートさんと国立国語研究所さんが日本語の学習済みモデルを作成し、さらに導入を超簡略化までしてくれました❕

GiNZAの導入はコマンドプロンプトなどで

```ruby:インストール
pip install ginza
```

で行うことができます。
導入は以上にして、ここからはコードを見ながら、AIの実装に移っていきたいと思います。プログラム全体は以下の通りです。

```python:VOSK_GiNZA.py
# 必要なモジュールをインポート
import json
import vosk as vs
import pyaudio
import wave
import spacy
import tkinter as tk

time=5 # 録音する時間
samplerate=44100 # サンプリング周波数
fs=1024 # 
index=1 # オーディオ機器の内、マイクの番号を指定する

# tkinterを用いてGUIを作成
root = tk.Tk() # ウィンドウを作成
root.title(u'VOSKとGiNZAを用いたボット') # タイトルの定義
root.geometry('500x500') # ウィンドウサイズを定義
frame=tk.Frame(root,bg='Green yellow') # テキストボックスなどを載せるフレームを定義
frame.pack() # フレームを設置
sc=tk.Scrollbar(frame) # スクロールバーの定義
sc.pack(side='right',fill='y') # スクロールバーを設置
msgs=tk.Listbox(frame,width=70,height=25,x=0,y=0,yscrollcommand=sc.set,bg='azure',fg='black') # テキストボックスの定義
msgs.pack(side='left',fill='both',pady=20) # テキストボックスの設置

nlp = spacy.load('ja_ginza') #GiNZAのロード

def record(index,samplerate,fs,time):
    pa=pyaudio.PyAudio() # pyaudioを読み込み
    global data 
    data=[] # 音声データを格納する配列
    dt=1/samplerate # 
    format=pyaudio.paInt16 # 量子化ビット数。大きくするほど解像度が良くなる
    stream=pa.open(format=format,channels=1,rate=samplerate, # channelsは使用するマイクの本数
                   input=True,input_device_index=index,frames_per_buffer=fs) # 録音機器を準備し、録音を開始する
    for i in range(int((time/dt)/fs)):
        frame=stream.read(fs) # time/dt/fsごとに音声を録音 
        data.append(frame) # dataの末尾に録音したデータを追加

    stream.stop_stream() #録音の終了
    stream.close() # 録音機器を消す
    pa.terminate() # pyaudioの終了
    wav=wave.open('voice.wav','wb') # 音声を記録する音声ファイルを開く
    wav.setnchannels(index) # ファイルはどのマイクを使用したか設定
    wav.setsampwidth(pa.get_sample_size(format)) # ファイルの量子化ビット数を設定
    wav.setframerate(samplerate) # ファイルのサンプリング周波数を設定
    wav.writeframes(b"".join(data)) # データをまとめる（ビット形式に書き換え）
    wav.close() # 音声ファイルを閉じる
    return data

def vos():  
    vs.SetLogLevel(-1) #vosk関連のログが出ないように指定
    wav=wave.open('voice.wav','rb') #音声ファイルの読み込み
    model= vs.Model("vosk_model") #日本語モデルの読み込み
    rec = vs.KaldiRecognizer(model, samplerate) # 日本語モデルを用いて音声を解析
    rec.AcceptWaveform(b"".join(data)) # 音声データの読み込み
    rec.SetWords(True) # 文章のセット
    output=rec.Result() # 結果を出力
    js=json.loads(output) # 結果（jsonファイル）の読み込み
    txt=js['text'] # テキスト部分のみ抽出
    return txt
    
def ret():
   data=record(index,samplerate,fs,time)
   txt=vos()
   i=0
   msgs.insert('end','あなた：'+txt)
   doc=nlp(txt) #GiNZAで入力文章を解析
   with open('ginza_voice.txt',encoding='utf-8_sig') as m: #想定返答集のロード（使う場合はファイル名を変更してください）
     r=m.read().split('\n') #\nで一つ一つの文章が区切られているので、\nごとに分解して配列化
  
   float_list=[] #計算した類似度を保持する配列
   for i in range(len(r)):
     k = nlp(r[i]) #想定返答集のi番目の文章の解析
     float_list.append(doc.similarity(k)) #類似度を計算し、float_listに格納
     i=i+1 
     j=float_list.index(max(float_list)) #最も類似度の高かった文章のインデックスを取得
   if max(float_list)<0.7: #類似度が低すぎる場合（ここの条件（０．７以上）は適宜変えてください）
     txt='僕はAIだからこういう時どう返せばいいかわからないかな。' 
     msgs.insert('end','えいみ：'+txt)
     
   if max(float_list)>0.7: #類似度がある程度高い場合
     txt=r[j]
     msgs.insert('end','えいみ：'+txt)
     
#ボタンとテキストボックスの定義
btn=tk.Button(root,text='声から生成した文章を送信',font=('utf-8_sig',10),bg='cyan',command=ret)
btn.pack(side='top')
#画面の保持
root.mainloop()   
```

まずtkinterを用いてGUIを作製していきます。
```python:GUI.py
# tkinterを用いてGUIを作成
root = tk.Tk() # ウィンドウを作成
root.title(u'VOSKとGiNZAを用いたボット') # タイトルの定義
root.geometry('500x500') # ウィンドウサイズを定義
frame=tk.Frame(root,bg='Green yellow') # テキストボックスなどを載せるフレームを定義
frame.pack() # フレームを設置
sc=tk.Scrollbar(frame) # スクロールバーの定義
sc.pack(side='right',fill='y') # スクロールバーを設置
msgs=tk.Listbox(frame,width=70,height=25,x=0,y=0,yscrollcommand=sc.set,bg='azure',fg='black') # テキストボックスの定義
msgs.pack(side='left',fill='both',pady=20) # テキストボックスの設置
```

その後、音声を録音して、音声ファイルを出力する関数recordを定義します。

```python:record.py
def record(index,samplerate,fs,time):
    pa=pyaudio.PyAudio() # pyaudioを読み込み
    global data 
    data=[] # 音声データを格納する配列
    dt=1/samplerate # 
    format=pyaudio.paInt16 # 量子化ビット数。大きくするほど解像度が良くなる
    stream=pa.open(format=format,channels=1,rate=samplerate, # channelsは使用するマイクの本数
                   input=True,input_device_index=index,frames_per_buffer=fs) # 録音機器を準備し、録音を開始する
    for i in range(int((time/dt)/fs)):
        frame=stream.read(fs) # time/dt/fsごとに音声を録音 
        data.append(frame) # dataの末尾に録音したデータを追加

    stream.stop_stream() #録音の終了
    stream.close() # 録音機器を消す
    pa.terminate() # pyaudioの終了
    wav=wave.open('voice.wav','wb') # 音声を記録する音声ファイルを開く
    wav.setnchannels(index) # ファイルはどのマイクを使用したか設定
    wav.setsampwidth(pa.get_sample_size(format)) # ファイルの量子化ビット数を設定
    wav.setframerate(samplerate) # ファイルのサンプリング周波数を設定
    wav.writeframes(b"".join(data)) # データをまとめる（ビット形式に書き換え）
    wav.close() # 音声ファイルを閉じる
    return data
```

色々複雑な変数の定義などを行っているので、もし使う場合はそのままにしておいてください。
次にVOSKを用いて文字起こしをするための関数vosを定義します。

```python:vos.py
def vos():  
    vs.SetLogLevel(-1) #vosk関連のログが出ないように指定
    wav=wave.open('voice.wav','rb') #音声ファイルの読み込み
    model= vs.Model("vosk_model") #日本語モデルの読み込み
    rec = vs.KaldiRecognizer(model, samplerate) # 日本語モデルを用いて音声を解析
    rec.AcceptWaveform(b"".join(data)) # 音声データの読み込み
    rec.SetWords(True) # 文章のセット
    output=rec.Result() # 結果を出力
    js=json.loads(output) # 結果（jsonファイル）の読み込み
    txt=js['text'] # テキスト部分のみ抽出
    return txt
```

文字起こししたテキストを出力してくれます。
最後にGiNZAを用いて入力と最も類似度の高い返答を返す関数retを定義します。

```python:ret.py
def ret():
   data=record(index,samplerate,fs,time)
   txt=vos()
   i=0
   msgs.insert('end','あなた：'+txt)
   doc=nlp(txt) #GiNZAで入力文章を解析
   with open('ginza_voice.txt',encoding='utf-8_sig') as m: #想定返答集のロード（使う場合はファイル名を変更してください）
     r=m.read().split('\n') #\nで一つ一つの文章が区切られているので、\nごとに分解して配列化
  
   float_list=[] #計算した類似度を保持する配列
   for i in range(len(r)):
     k = nlp(r[i]) #想定返答集のi番目の文章の解析
     float_list.append(doc.similarity(k)) #類似度を計算し、float_listに格納
     i=i+1 
     j=float_list.index(max(float_list)) #最も類似度の高かった文章のインデックスを取得
   if max(float_list)<0.7: #類似度が低すぎる場合（ここの条件（０．７以上）は適宜変えてください）
     txt='僕はAIだからこういう時どう返せばいいかわからないかな。' 
     msgs.insert('end','えいみ：'+txt)
     
   if max(float_list)>0.7: #類似度がある程度高い場合
     txt=r[j]
     msgs.insert('end','えいみ：'+txt)
```

以上が音声認識して文字起こしして、自然な返答を返すAIの概要です。
認識精度の問題で早口だとうまく認識してくれません。もし用いる場合はゆっくり、活舌よく話しかけてみてください。

では、ばいにゃん～




参考にさせていただいたもの
【Python】VOSKによるスピーカーからの音声出力のオフライン文字起こし
https://qiita.com/3998/items/a18b50aaf58e0176e6a5
PythonのPyAudioで音声録音する簡単な方法
https://watlab-blog.com/2019/06/20/pyaudio-record/