VOSK+MeCabで無理やり辞書追加みたいなことをする

仕事で音声認識出来ないか？と言われて実験がてらやってみた記録です

# 調査
手軽に使える音声認識として
・web speech api
・julius
・VOSK

などあるかなと思います。

使ってみた実感として、認識の精度は

web speech api > VOSK >>julius　という感じでした

ただ、専門用語（業界特有の略語）の文字起こしするにはどうしても辞書に語彙追加みたいなのをしないと
いけません。juliusは辞書への追加が簡単だけど精度がイマイチ、、web speech apiは出来そうにない。。
VOSKは出来ると公式に書いてあるけどなんか敷居が高そう。。

そこであまりかっこよくは無いけど、下記のような組み合わせでやってみることにしました


## 事前準備
１，VOSKに専門用語を聞かせて、認識した文字列をメモ。。

２，MeCabのユーザー辞書にメモった文字列を単語として登録。（※）

３，２の単語と、認識してほしい単語の対応表をつくる

## 準備が終わったら
１，VOSKで認識した文字列を、MeCabで分かち書き

２，対応表を元に、認識してほしい単語に置き換える


（※）なんでこんな回りくどいかというと、専門用語としては単語一つなのにVOSKで認識されたときには複数の単語の組み合わせとして
　　　認識されることが多く、分かち書き→置き換えがうまく出来ないからです

MeCabのユーザー辞書に登録するのは、ほとんど意味をなさない単語のつらなりなので、そのために誤変換起こすとかいう事は
あまり無いのではないかなと思います

## 必要なファイル等


# MeCabユーザー辞書登録単語リスト
```csv:my.csv
上ザー,,,1,名詞,一般,*,*,*,*,上ザー,ウエザー,ウエザー
```

# MeCabのユーザー辞書登録
MeCabのシステム辞書は環境によって違うと思います。
```sh:mecab-dict-index.sh
sudo /usr/lib/mecab/mecab-dict-index -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -u /home/test/my.dic -f utf8 -t utf8 /home/test/my.csv
```

# 対応表
左がMeCabに登録した単語、右が出てほしい単語
```sh:change_table.txt
上ザー ウェザー
```

# my.dicの指定方法（python）
```python:mydic.py
tagger=MeCab.Tagger('-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -u /home/test/my.dic')
```

# 全体のスクリプト
VOSKインストールするとついてくるexampleを改造しただけ・・
環境として
・ubuntu20.04
・python3.9
です

```python:vosk_mydic.py
import argparse
import os
import queue
import sounddevice as sd
import vosk
import sys
import MeCab
import ipadic
import time
from multiprocessing import Process
import multiprocessing
import tkinter

M_SIZE=1024
tagger=MeCab.Tagger('-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -u /home/test/my.dic')


q = queue.Queue()
text=multiprocessing.Queue()
table=open("/home/test/cange_table.txt","r")
change={}
for i in table:
    (k,v)=i.split()
    change[k]=v
print(change)
table.close()
class Monitor:
    def __init__(self):
        self.Monitor=tkinter.Tk()
        self.Monitor.title("MOJI_Monitor")
        self.Monitor.geometry("1920x500")
        self.Monitor.configure(bg="black")
        self.label=tkinter.Message(self.Monitor,font=("HackGenNerd-Regular","35"),width=1800,text="Speech To Text from vosk")
        self.Monitor.after(100,self.update)
        self.Monitor.mainloop()
    def update(self):
        #self.label.destroy()
        t=text.get()
        if(len(t)<=3):
            t=""
        t=t.replace(" ","")
        c=""
        for i in tagger.parse(t).split(" "):
            if(i in change):
                i=change[i]
            c=c+i
        print("original >> ",t)
        print("modify   >> ",c)
        d=len(c)/50
        moji=""
        for i in range(int(d)+1):
            moji=moji+c[0+(50*i):50+(50*i)]+"\n"
        self.label=tkinter.Label(self.Monitor,font=("HackGenNerd-Regular","35"),width=60,text=moji,background="#000000",justify="left",anchor="w",foreground="#c6c6c6")
        self.label.place(x=0,y=0)
        self.Monitor.after(100,self.update)


def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text

def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(bytes(indata))

M=Process(target=Monitor)
M.start()



parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    '-l', '--list-devices', action='store_true',
    help='show list of audio devices and exit')
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser])
parser.add_argument(
    '-f', '--filename', type=str, metavar='FILENAME',
    help='audio file to store recording to')
parser.add_argument(
    '-m', '--model', type=str, metavar='MODEL_PATH',
    help='Path to the model')
parser.add_argument(
    '-d', '--device', type=int_or_str,
    help='input device (numeric ID or substring)')
parser.add_argument(
    '-r', '--samplerate', type=int, help='sampling rate')
args = parser.parse_args(remaining)

try:
    if args.model is None:
        args.model = "model"
    if not os.path.exists(args.model):
        print ("Please download a model for your language from https://alphacephei.com/vosk/models")
        print ("and unpack as 'model' in the current folder.")
        parser.exit(0)
    if args.samplerate is None:
        device_info = sd.query_devices(args.device, 'input')
        # soundfile expects an int, sounddevice provides a float:
        args.samplerate = int(device_info['default_samplerate'])

    model = vosk.Model(args.model)

    if args.filename:
        dump_fn = open(args.filename, "wb")
    else:
        dump_fn = None

    with sd.RawInputStream(samplerate=args.samplerate, blocksize = 8000, device=args.device, dtype='int16',
                            channels=1, callback=callback):
            print('#' * 80)
            print('Press Ctrl+C to stop the recording')
            print('#' * 80)

            rec = vosk.KaldiRecognizer(model, args.samplerate)
            while True:
                data = q.get()
                if rec.AcceptWaveform(data):
                    result=rec.Result().split("\n")[1].split(":")[1]
                    text.put(result)
                else:
                    pass
                    #print(rec.PartialResult())
                if dump_fn is not None:
                    dump_fn.write(data)

except KeyboardInterrupt:
    print('\nDone')
    parser.exit(0)
except Exception as e:
    parser.exit(type(e).__name__ + ': ' + str(e))


```

## デモ

![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90606/0cab4be6-be6d-eb5b-4153-412a4411b687.gif)

音が無いのでなんの事やら。。かも




## 最後に
VOSKで辞書に無い単語の認識を行った場合、いつも一定の単語が帰ってくる訳ではないので、、
それなりに労力いりそうで現実的ではないかもしれません。。

VOSKの辞書に登録されている単語、、なにこれ？というのがたくさんあって面白いです

![qiita.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90606/73ba230b-fd00-85b9-2b8a-aa44f0cc0143.jpeg)