AnyTech Engineer Blog

AnyTech Engineer Blogは、AnyTechのエンジニアたちによる調査や成果、Tipsなどを公開するブログです。

JARVIS(っぽい何か)を作ろう!第一回:マイクからリアルタイムで音声認識する

JARVIS(っぽい何か)を作ろう!第一回:マイクからリアルタイムで音声認識する

はじめに

おはようございます!AnyTechの渡邉です。最近はAIとお話しすることにハマってます。
本記事はおしゃべりAIをオフラインかつローカルで実装するシリーズです。

JARVISって?

映画アイアンマンに登場する、主人公を日常・戦闘・開発のあらゆる状況で助けてくれる架空の人工知能です。
声で話しかけて、それに応じてJARVISが情報を提供したり、アイアンマンを自律制御したりサポートしてくれます。
主人公の良き相棒であり、頼れる存在です。

シリーズ

こちらの手順を踏まえて、作っていきたいと思います。
※ 本シリーズはローカルUbuntuマシンにGPUがある場合を想定しております。

第一回:マイクからリアルタイムで音声認識する
第二回:音声認識した結果から返答生成する
第三回:返答を合成音声で喋らせる
第四回:キャラクターが喋っているような見た目を作る
第五回:今までで作ったものを組み合わせる

第一回:音声認識

日本語に対応していて、オフラインで音声認識する方法はいくつかあります。
- RealSense SDK
- ESPNet2
- Whisper

今回は精度も高く推論も速く、個人的にも長年お世話になっているESPNet2を使用させていただきます。

ESPNetは、End-to-Endの音声処理のツールであり、音声認識、合成音声、音声言語理解など幅広く音声言語系をカバーしています。実装はPyTorchでCNN、RNN、LSTM、Transformerなどのモデルが実装されています。

何を作るか

音声認識を常時起動しておいて、一定の音量を超えると録音して音声認識をしてテキストを標準出力する処理を作ります。
テキストを標準出力した後、再び音声認識を機動するループへ戻ります。

環境準備

必要なライブラリをインストールします。

sudo apt-get install libasound-dev portaudio19-dev libportaudio2 libportaudiocpp0
sudo apt-get install ffmpeg libav-tools
pip install PyAudio==0.2.11
pip install SoundFile==0.10.3
pip install espnet==0.10.1 
pip espnet-model-zoo==0.1.7 

PyTorch系のバージョン:

torch==1.7.1+cu110
torch-complex==0.2.1
torch-optimizer==0.1.0
torch-summary==1.4.5
torchaudio==0.7.2
torchvision==0.8.2+cu110

処理の流れ

  1. マイクで音声を受け付ける
  2. 音量が閾値以上になったら録画開始
  3. 音量が下がったら録画した音声を保存
  4. 保存した音声を音声認識で日本語に文字起こして、1に戻る

コード

マイクから音声を受け取り、文字起こしするコードです。 tools/voice_recognize.py

import os
import sys
import soundfile
import pyaudio
import wave
import numpy as np
from espnet_model_zoo.downloader import ModelDownloader
from espnet2.bin.asr_inference import Speech2Text
FORMAT = pyaudio.paInt16

# Audio params
chunk = 1024  #1つの音声チャンクの長さを1024サンプルとする
threshold = 0.50  #マイクの音量がこれを超えると録音を開始する
sampling_rate = 16000  #サンプリングレート、マイク性能に依存
record_seconds = 15  #録音時間の上限
record_cache_seconds = 0.9
stop_seconds = 2  #録音中断までの時間
B = int(sampling_rate / chunk * stop_seconds)
A = int(sampling_rate) / int(chunk) * int(record_seconds)
A_CACHE = int(sampling_rate) / int(chunk) * int(record_cache_seconds)


def load_voice_recog_model():
    d = ModelDownloader()
    model = Speech2Text(
        **d.download_and_unpack("Shinji Watanabe/laborotv_asr_train_asr_conformer2_latest33_raw_char_sp_valid.acc.ave"),
        device="cuda"
    )
    return model


def save_wave(all, audio_path):
    data = b''.join(all)
    out = wave.open(audio_path,'w')
    out.setnchannels(1) #mono
    out.setsampwidth(2) #16bits
    out.setframerate(sampling_rate)
    out.writeframes(data)
    out.close()


def inference(speech2text, audio_path):
    speech, _ = soundfile.read(audio_path)
    nbests = speech2text(speech)
    text, *_ = nbests[0]
    return text


def recognize(speech2text):
    audio_path = "speech.wav"
    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT, channels=1, rate=sampling_rate, input=True, frames_per_buffer=chunk)
    all = []
    while True:
        #まずサンプルを取る
        data = stream.read(chunk, exception_on_overflow=False)
        x = np.frombuffer(data, dtype="int16") / 32768.0
        xmax = x.max()
        print(xmax)
        if xmax <= threshold:
            all = []
            for i in range(0, int(A_CACHE)):
                data_cache = stream.read(chunk, exception_on_overflow=False)
                all.append(data_cache)
        else:
            n = 0  #連続して閾値を下回った回数を記録するための変数
            for i in range(0, int(A)):
                data = stream.read(chunk, exception_on_overflow=False)
                all.append(data)
                x = np.frombuffer(data, dtype="int16") / 32768.0
                xmax = x.max()
                if xmax <= threshold:
                    n += 1  #閾値を下回っていたらincrementする
                else:
                    n = 0  #閾値を上回ったらリセットする
                # 中断判定
                if n == B:
                    break
            # 音声保存
            save_wave(all, audio_path)
            all = []

            # 音声認識
            text = inference(sr_model, audio_path)
            print(f"Recognized text = {text}")


if __name__ == '__main__':
    sr_model = load_voice_recog_model()
    recognize(sr_model)

実行

python tools/voice_recognize.py

まとめ

手元にGPUマシンがあれば、音声認識できてしまうのはすごいですよね! 次回は音声認識した結果に返答する処理の紹介をする予定です。

次の記事:JARVIS(っぽい何か)を作ろう!第二回:音声認識した結果から返答生成する