生成AIをローカルで簡単に 【Part5.5 faster-whisper+マイク録音編】
はじめに
今回は、初心者向けにGoogle Colaboratoryローカル環境で、簡単に生成AIを使えるようにする環境を作ります。
Part5では、Google colabにて、同じく、音声認識AIであるfaster-whisperを利用して、音声ファイルを文字起こししましたが、Part5.5である今回は、マイクから録音された音声をそのまま文字起こしすることを検討します。
(音声対話システムを作成するために必要なため)
マイクから音声を録音することは、 Google Colabでも可能ではあるのですが、ローカルで音声を録音する方法とGoogle Colabで音声を録音する方法が大きく異なるため、今回は実用性を重視して、ローカルで録音して、文字起こしを実施します。
本記事は、Part5の記事の内容がわかっていることを前提に記載します。
まだ、Part5を読んでいない方は、そちらも合わせて読んでいただくことを推奨します。
成果物
下記のリポジトリをご覧ください。
解説
下記の通り、解説を行います。
まずは上記のリポジトリをcloneしてください。
git clone https://github.com/personabb/colab_AI_sample.git
その後、cloneしたフォルダ「colab_AI_sample」をローカルの適当なフォルダにおいてください。
ディレクトリ構造
ローカルのディレクトリ構造は下記を想定します。
(今回の記事で利用するもののみ記載します)
hogehoge/
    └ colab_AI_sample/
          └ colab_fasterwhisper_sample/
                  ├ configs/
                  |    └ config.ini
                  ├ module/
           |    ├ module_recoder.py
                  |    └ module_whisper.py
                  ├ requirements.txt
                  └ main.py
- 
colab_AI_sampleフォルダは適当です。なんでも良いです。1階層である必要はなく下記のように複数階層になっていても良いです。MyDrive/hogehoge/spamspam/hogespam/colab_AI_sample
 - 
moduleフォルダには、faster-whisperを動かすためのモジュールと、マイクから音声を録音するためのモジュールが入っている。- 
configs/config.iniは設定ファイルであり、モデルや録音を動作させるために必要な設定が記載されています。 - 
requirements.txtは環境構築用に必要なファイルです。 
 - 
 
事前準備(環境構築)
大前提として、pythonのバージョンは3.10もしくは3.11を想定します。
その他のバージョンでは動作確認していません。
前提として下記のコマンド実行後のディレクトリで作業してください。
cd colab_AI_sample/colab_fasterwhisper_sample
pythonのバージョン指定
pythonのバージョンはpyenvで指定します。
pyenv自体の導入については下記をご覧ください。
pyenvが導入できていれば、下記のコマンドでpythonのバージョンを指定できます。
pyenv install 3.10.14 #もしくは3.11.9など
pyenv local 3.10.14 #もしくはpyenv global 3.10.14
これでpythonのバージョンが指定できます。
pyenv globalはシステム全体に、このバージョンを反映させたい時に利用してください。
pyenv localは現在のカレントディレクトリでのみ、このバージョンを反映させたい場合に利用します。
下記コマンドを実行して、pythonのバージョンが変更されているかを確認してください。
python -V
# Python 3.10.14
必要なパッケージのインストール
続いて、必要なパッケージをインストールするために仮想環境を構築します
venvで仮想環境を構築します。
venvはpython公式の仮装環境のため、pythonが利用可能であれば導入の必要なく利用できます。
python -m venv env
source env/bin/activate
pip install -r requirements.txt
最後のスクリプトを実行することで、各種重み情報を取得できます。
設定ファイルの設定
デフォルトの設定ファイルのまま、利用する場合は特に変更を加える必要はないですが、こちらの設定ファイルの中身を変更することで、異なるモデルを試したり、録音の環境を変えることができます。
設定ファイルは下記になります
[Recorder]
fs=16000
silence_threshold=0.5
min_duration=0.1
amplitude_threshold=0.05
start_threshold = 0.3
[FasterWhisper]
device = auto
language = ja
gpu_model_type = large-v3
gpu_beam_size = 1
gpu_compute_type = float16
cpu_model_type = small
cpu_beam_size = 1
cpu_compute_type = int8
use_kotoba = True
kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster
chunk_length = 15
condition_on_previous_text = False
これまでのPart1からpart5でも利用していた設定ファイルです、これまでは Google Colab上で設定ファイルの中身を書き換えていましたが、今回はローカル動作のため設定ファイル自体を直接いじっていきます。
まず[Recorder]部分は録音モジュールに関係する設定です。
- 
fs=16000- サンプリングレート16,000Hzで録音します。Whisperが16,000Hzの音声を前提としているため、このままで良いです。
- 変更しても良いですが、モデルの中で勝手に16,000Hzに前処理されてから文字起こしされるため、大きくしたからといって文字起こし精度が高くなるわけではないです。
 
 
 - サンプリングレート16,000Hzで録音します。Whisperが16,000Hzの音声を前提としているため、このままで良いです。
 - 
silence_threshold=0.5- 無音になってから0.5秒後に録音を停止するパラメータです。
 
 - 
min_duration=0.1- streamで録音する際の、chunkの大きさです。変更の必要はないです。
 
 - 
amplitude_threshold=0.05- 無音判定の閾値です。音声の音量が0.05よりも小さい音は無音と判定します。
 - これが0だと、自然なノイズも全て音と判定するため、永遠に録音が終わりません。
 
 - start_threshold = 0.3
- 閾値(
amplitude_threshold)以上の音量が、0.3秒以上継続して入ってきたら、発話開始と判断して録音を開始します。 - 突発的なノイズによる閾値越えを防ぐための処理です。
 
 - 閾値(
 
[FasterWhisper]部分は文字起こしモジュールの設定です。
- 
device = auto- マシンのGPUやCPUのどちらを利用するかの設定
- 
autoの場合は、マシンがGPUを利用できるならGPUを利用する。 - その他では
cudaやcpuを指定できる。 
 - 
 
 - マシンのGPUやCPUのどちらを利用するかの設定
 - 
language = ja- 言語設定を指定している。ここでは日本語を設定している。
 
 - 続いての6行はモデルの設定をしている
- GPUを利用する場合とCPUを利用する場合でモデルを変更している
- GPU利用の場合は
large-v3モデルという高性能モデルを利用している - CPU利用の場合は
smallモデルという軽量モデルを利用している- 加えてCPUの場合は、
int8という小さなデータ型を利用して、計算量を減らしている 
 - 加えてCPUの場合は、
 
 - GPU利用の場合は
 
 - GPUを利用する場合とCPUを利用する場合でモデルを変更している
 - 
use_kotoba = True- 日本語特化モデルである
kotoba-whisper-v1.0を利用するかどうかを設定している。 - このモデルの方が処理速度が速いため、おすすめである。
 - 精度と処理速度はいろんなモデルを試して比較してみてください。
 
 - 日本語特化モデルである
 - 
kotoba_model_type = kotoba-tech/kotoba-whisper-v1.0-faster- 利用するfaster-whisperモデルを指定している。
 - ここを変更することで、異なるモデルを試すことができる。
- ただし
use_kotoba = Trueの場合のみ 
 - ただし
 
 - それ以外は変更の必要性はないです。
 
使い方解説
環境構築後、下記コマンドでプログラムを実行できます。
main.py
その後、stand by ready OKとターミナル上で表示されたら、マイクに向かって発話してください。
発話をやめたら、録音が停止され、その内容が文字起こしされてコンソールに表示されます。
Ctrl+Cをおしてプログラムを停止するまで、永遠に録音と文字起こしが繰り返されます。
コード解説
主に、重要なmain.py、module_recoder.py、module_whisper.pyについて解説します。
main.py
該当のコードは下記になります。
コード全文
import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        audio_data = recorder.speech2audio()
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)
if __name__ == "__main__":
    main()
下記で解説します。短いコードなので一瞬です。
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
で、moduleフォルダ内の録音用のモジュールと文字起こし用のモジュールを呼び出しています。
recorder = Recorder()
fasterWhispermodel = FasterWhisperModel()
呼び出したモジュールのクラスのインスタンスを作成します。
    while True:
        audio_data = recorder.speech2audio()
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)
ここで、recorder.speech2audio()メソッドで音声を録音して、録音データをaudio_dataに格納し、fasterWhispermodel.audio2text()メソッドにて、音声ファイルを文字起こしして、textとして表示しています。
続いては、該当のモジュールの中身を説明します。
module_recoder.py
このモジュールは、発話の録音用モジュールです。
下記にコードを提示します。
コード全文
import numpy as np
import sounddevice as sd
import os
import configparser
# ファイルの存在チェック用モジュール
import errno
class Recorderconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)
class Recorder:
    def __init__(self, config_ini_path = './configs/config.ini'):
            
            Recorder_config = Recorderconfig(config_ini_path = config_ini_path)
            config_dict = Recorder_config.Recorder_config_dict
            
            self.fs = int(config_dict["fs"])
            self.silence_threshold = float(config_dict["silence_threshold"])
            self.min_duration = float(config_dict["min_duration"])
            self.amplitude_threshold = float(config_dict["amplitude_threshold"])
            self.start_threshold = float(config_dict["start_threshold"])
    def speech2audio(self):
        record_Flag = False
        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >=10:
                    if np.max(np.abs(data) > self.amplitude_threshold) and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio=non_recorded_data[int(-1*start_threshold*10)-2:]  
                    else:
                        input_time = 0
                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)
                    else:
                        non_recorded_data.append(data)
                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if (silent_time >= self.silence_threshold) and record_Flag:
                            print("finished")
                            record_Flag = False
                            break
                    else:
                        silent_time = 0
        audio_data = np.concatenate(recorded_audio, axis=0)
        return audio_data
では一つ一つ解説します。
Recorderconfigクラス
class Recorderconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)
ここではconfig_ini_path = './configs/config.ini'で指定されている設定ファイルをRecorder_config_dictとして読み込んでいます。
辞書型で読み込んでいるため、設定ファイルの中身をpythonの辞書として読み込むことが可能になります。
Recorderクラスのinitメソッド
class Recorder:
    def __init__(self, config_ini_path = './configs/config.ini'):
            
            Recorder_config = Recorderconfig(config_ini_path = config_ini_path)
            config_dict = Recorder_config.Recorder_config_dict
            
            self.fs = int(config_dict["fs"])
            self.silence_threshold = float(config_dict["silence_threshold"])
            self.min_duration = float(config_dict["min_duration"])
            self.amplitude_threshold = float(config_dict["amplitude_threshold"])
            self.start_threshold = float(config_dict["start_threshold"])
まず、設定ファイルの内容をconfig_dictに格納しています。これは辞書型のため、config_dict["device"]のような形で設定ファイルの内容を文字列として取得することができます。
あくまで、すべての文字を文字列として取得するため、int型やbool型にしたい場合は、適宜型変更をする必要があることに注意してください。
続いて、設定ファイルから各種値をインスタンス変数に格納する。
Recorderクラスのspeech2audioメソッド
class Recorder:
   ・・・
    def speech2audio(self):
        record_Flag = False
        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >=10:
                    if np.max(np.abs(data) > self.amplitude_threshold) and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio=non_recorded_data[int(-1*start_threshold*10)-2:]  
                    else:
                        input_time = 0
                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)
                    else:
                        non_recorded_data.append(data)
                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if (silent_time >= self.silence_threshold) and record_Flag:
                            print("finished")
                            record_Flag = False
                            break
                    else:
                        silent_time = 0
        audio_data = np.concatenate(recorded_audio, axis=0)
        return audio_data
上記の部分では,ユーザの発話をマイクを通して取得しています。
「stand by ready OK」と表示されてから,マイクに入力された音声の大きさが閾値(self.amplitude_threshold)以上だった場合に,録音が開始されます。
録音が開始されたら、「recording...」と表示されます
その後、マイクに入力された音声の大きさが閾値以下になって0.5秒(self.silence_threshold)経過したら録音が停止するように設計されています。
録音が停止したら、「finished」と表示されます。
module_whisper.py
続いてのモジュールは、音声の文字起こし用のモジュールです。
本モジュールはPart5の記事に記載されているモジュールとほとんど同じなため、差分だけ説明することにします。
下記にコードを提示します。
コード全文
from faster_whisper import WhisperModel
import numpy as np
import torch
import os
import configparser
# ファイルの存在チェック用モジュール
import errno
class FasterWhisperconfig:
    def __init__(self, config_ini_path = './configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        FasterWhisper_items = self.config_ini.items('FasterWhisper')
        self.FasterWhisper_config_dict = dict(FasterWhisper_items)
class FasterWhisperModel:
    def __init__(self,device = None, config_ini_path = './configs/config.ini'):
        FasterWhisper_config = FasterWhisperconfig(config_ini_path = config_ini_path)
        config_dict = FasterWhisper_config.FasterWhisper_config_dict
        if device is not None:
            self.DEVICE = device
        else:
            device = config_dict["device"]
            self.DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
            if device != "auto":
                self.DEVICE = device
            
        self.BEAM_SIZE = int(config_dict["gpu_beam_size"]) if self.DEVICE == "cuda" else int(config_dict["cpu_beam_size"])
        self.language = config_dict["language"]
        self.COMPUTE_TYPE = config_dict["gpu_compute_type"] if self.DEVICE == "cuda" else config_dict["cpu_compute_type"]
        self.MODEL_TYPE = config_dict["gpu_model_type"] if self.DEVICE == "cuda" else config_dict["cpu_model_type"]
        self.kotoba_chunk_length = int(config_dict["chunk_length"])
        self.kotoba_condition_on_previous_text = config_dict["condition_on_previous_text"]
        if self.kotoba_condition_on_previous_text == "True":
            self.kotoba_condition_on_previous_text = True
        else:
            self.kotoba_condition_on_previous_text = False
        if config_dict["use_kotoba"] == "True":
            self.use_kotoba = True
        else:
            self.use_kotoba = False
        if not self.use_kotoba:
            self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.COMPUTE_TYPE)
        else:
            self.MODEL_TYPE = config_dict["kotoba_model_type"]
            #self.model = WhisperModel(self.MODEL_TYPE, device=self.DEVICE, compute_type=self.cotoba_compute_type)
            self.model = WhisperModel(self.MODEL_TYPE)
    def audio2text(self, data):
        result = ""
        data = data.flatten().astype(np.float32)
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text
        
        return result
            
    def audioFile2text(self, file_path):
        result = ""
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(file_path, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(file_path, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text
        return result
では一つ一つ解説していきます。
FasterWhisperconfigクラス
Part5の記事をご覧ください。全く同じです。
FasterWhisperModelクラスのinitメソッド
Part5の記事をご覧ください。全く同じです。
FasterWhisperModelクラスのaudioFile2textメソッド
今回の記事では使いません。こちらはPart5の記事で、音声ファイルから文字起こしを行う際に利用したメソッドです。詳細はPart5の記事をご覧ください。全く同じです。
FasterWhisperModelクラスのaudio2textメソッド
class FasterWhisperModel:
    ・・・
    def audio2text(self, data):
        result = ""
        data = data.flatten().astype(np.float32)
        if not self.use_kotoba:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language)
        else:
            segments, _ = self.model.transcribe(data, beam_size=self.BEAM_SIZE,language=self.language, chunk_length=self.kotoba_chunk_length, condition_on_previous_text=self.kotoba_condition_on_previous_text)
        
        for segment in segments:
            result += segment.text
        
        return result
基本的にはPart5で使ったメソッドと同じで、faster-whisperモデルのtranscribeメソッドを呼び出して、音声認識をしています。
設定ファイルで指定したモデルに合わせて、適切な引数を使っています。
faster-whisperは30秒以上の音声に関しては、音声を分割して処理をするため、分割されて生成されたテキストをresult変数に格納して、returnしています。
また、
data = data.flatten().astype(np.float32)
上記の部分で、音声データをfaster-whisperモデルに入力するために、データを整形しています。
まとめ
今回は、初心者向けにGoogle Colaboratoryローカル環境で、簡単に生成AIを使えるようにする環境を作りました。
Part5.5の今回は、ローカルで音声をマイクから録音して、その音声を、音声認識AIであるfaster-whisperによって文字起こししてみました。
音声対話システムを作るためには、マイクからの録音とその文字起こしが必須なので、 Google Colabではないですが、一旦取り扱いました。
次回以降はまた Google Colabに戻ります。
さらに、音声認識AIを利用した音声対話システムに関しても、下記で記事を書いています。
もし興味があれば一読いただけますと幸いです。
次回Part6では、画像生成AI Stable-Diffusionを使えるようにしたいと思います。
Discussion