VOSKとargostranslateとpyttsx3でオフライン通訳させてみました

# はじめに  

- 最近のAIは学習済みでツールとして使えるようになったものがあります。
例えば、音声認識ツール：__VOSK__、翻訳ツール：__argostranslate__
- これらと読上げライブラリ：__pyttsx3__ をくっつけて、音声認識ー翻訳ー読み上げをしてみました。  
- これらはオフラインで動くツールで、その実力を試してみました。

### 工夫したこと
1. ローカル化  
　ネットワーク環境に縛られないツールを組み合わせました。
1. 翻訳ループ防止
　そのまま繋げたら、PCが自分で読上げた音声を再認識して、再翻訳して、再読上げして、、、と無限ループしたので、読上げ中は認識を止めるようにしました。

# 内容
### 1. PC環境
　実施したPC環境は以下のとおりです。 

| | |
|:--:|:--:|
|CPU|Celeron N4100|
|メモリ|8GB LPDDR4|

### 2. 前準備
　Windowsで使えるようにするため、以下のツール / システムをインストールしました。
インストール時に参考にしたサイトも記載します。  
1. python（使用したのはVer.3.7 ）
　実行時のベースシステムです。
（参考）https://qiita.com/ssbb/items/b55ca899e0d5ce6ce963
1. pip（使用したのはVer.21.2.4 ）
　他のツールをダウンロードする際に使うツールです。
（python3系ではバージョン3.4以降であれば、pythonのインストールと共にpipもインストールされます。）
（参考）https://gammasoft.jp/python/python-library-install/

1. VOSK（使用したのはVer.0.3.42 ）
　音声認識ツールです。
（参考）https://alphacephei.com/vosk/
1. VOSK models（使用したのはvosk-model-small-ja-0.22 ）
　日本の音声認識で使用するモデルです。
　展開したファイル群を**VOSK_try/models/ja**というフォルダに入れました。
（参考）https://alphacephei.com/vosk/models
1. sounddevice（使用したのはVer.0.4.4 ）
　マイクを使うためのライブラリです。
（参考）https://www.wizard-notes.com/entry/python/sounddevice
1. argostranslate（使用したのはVer.1.7.0）
　翻訳ツールです。
（参考）https://portal.kemu.site/blog/1258346492
1. pyttsx3（使用したのはVer.2.90 ）
　読上げライブラリです。
（参考）https://pypi.org/project/pyttsx3/

　また、作成にあたり、以下のサイトを参照しました。
1. VOSK：
（参照）https://qiita.com/hatt_takumi/items/0aedc8f6768cd8efee5e
（参照）https://github.com/alphacep/vosk-api/blob/master/python/example/test_simple.py
（参照）https://github.com/alphacep/vosk-api/blob/master/python/example/test_microphone.py
1. argostranslate：
（参照）https://www.argosopentech.com/
1. pyttsx3
（参照）https://qiita.com/be_tiger/items/370ce30a81f4b420f0dd

### 3.pyファイルの作成
 組んだコードは次のとおりです。

~~~python
#--- VOSK 初期化 ---
import argparse
import os
import queue
import sounddevice as sd
import vosk
import sys

#---
#--- argotranslate 初期化 ---
import argostranslate.package, argostranslate.translate

from_code = "ja"
to_code = "en"

# Download and install Argos Translate package
available_packages = argostranslate.package.get_available_packages()
available_package = list(
    filter(
        lambda x: x.from_code == from_code and x.to_code == to_code, available_packages
    )
)[0]
download_path = available_package.download()
argostranslate.package.install_from_path(download_path)

# Translate
installed_languages = argostranslate.translate.get_installed_languages()
from_lang = list(filter(
	lambda x: x.code == from_code,
	installed_languages))[0]
to_lang = list(filter(
	lambda x: x.code == to_code,
	installed_languages))[0]
translation = from_lang.get_translation(to_lang)
translation.translate("私")#dummy 'input over flow'を表示させないため

#---
#--- pyttsx3 初期化 ---
import pyttsx3
engine = pyttsx3.init()

#rate
rate = engine.getProperty('rate')   # getting details of current speaking rate
print (rate)                        #printing current voice rate
engine.setProperty('rate', 125)     # setting up new voice rate

#volume
volume = engine.getProperty('volume')   #getting to know current volume level (min=0 and max=1)
print (volume)                          #printing current volume level
engine.setProperty('volume',1.0)    # setting up volume level  between 0 and 1

#voice
voices = engine.getProperty('voices')       #getting details of current voice

#---

# rec : VOSKの音声認識エンジン
# translation : argostranslateの翻訳エンジン
# engine : pyttsx3の読上げエンジン
#--- メインルーチン ---
q = queue.Queue()

rec_flg = True

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)
    if rec_flg:
        q.put(bytes(indata))

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(
    '-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.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("../VOSK_try/models/ja")

    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)

# -- メインループ start --
            while True:
                data = q.get()
                if rec.AcceptWaveform(data):

#--- 音声認識の結果から文字だけ抽出
                    txt = rec.Result()
                    txt2 = txt.split("\"")[3]
                    txt2 = txt2.replace(' ','')
#---
                    if len(txt2) > 0:
                        rec_flg = False
#--- 認識結果の表示・読み上げ
                        print(txt2)
                        engine.setProperty('voice', voices[0].id)  #changing index, changes voices. o for japanese female
                        engine.say(txt2)
#---
                        engine.setProperty('voice', voices[1].id)  #changing index, changes voices. o for english female
#--- argostranslateでの翻訳と表示・読み上げ
                        translatedText = translation.translate(txt2)
                        print(translatedText)
                        engine.say(translatedText)
#---
                        engine.runAndWait() #engineが終わるまで待つ
                        engine.stop()
                        rec_flg = True

                if dump_fn is not None:
                    dump_fn.write(data)
# -- メインループ end --

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

### 4.説明

#### 1. ローカル化
 　3つのツールを組み合わせただけなので、特記なしです。

#### 2. 翻訳ループ防止
 **rec_flg**というBOOL変数を入れ、これが**True**のときだけ**indata**を記録するように変更、

~~~python
    if rec_flg:
        q.put(bytes(indata))
~~~

　読上げ前に**rec_flg**を**False**に、読上げ後**True**に変更するようにしました。

~~~python
                        rec_flg = False
                        
                        #省略（読上げ処理コード）
                        
                        rec_flg = True
~~~

### 5.結果
- 同時通訳とはいきませんが、まあまあな速さで処理できました。
- 音声認識は、はきはき話せば大体正しく伝わりました。数字はあまり得意ではなさそうです。
- 翻訳は少々意訳されるように感じました。
例えば「私は今日東京駅に行きます」が「I go to Tokyo Station」になりました。
- 今回は日本語から英語への変換ですが、様々な言語が用意されてます。

# 参考

参考コード
https://github.com/alphacep/vosk-api/blob/master/python/example/test_microphone.py