はじめに
ういっすういっすういっすー!✌️ ぱー🖐 nikkieです。
シオン・プロジェクト、Whisper章の続編です。
観測範囲ではいろんな方がWhisperを触られてますね〜。
Whisperを使えば短いコードで音声ファイルを書き起こせます!
今回は「音声ファイルではなく、マイクから入力した音声は書き起こせるの?」という疑問にアプローチします。
目次
- はじめに
- 目次
- 前回のシオン・プロジェクト!:環境構築
- 結論:Whisperはマイクからも音声認識できます!
- 「何度でも認識させよう」のモチベーション
- 音声データの渡し方を調査:transcribeメソッドは何をしているのか
- マイクから取得した音声データの扱い方
- ファイル出力を使った場合と同じ入力になっているか検証
- 終わりに
- P.S. もう一つのモチベーション
前回のシオン・プロジェクト!:環境構築
前回構築した環境を使っていきます。
ただし、whisperのソースコードを最新化したく、whisperだけ再インストールしました。
$ pip uninstall -y whisper $ pip install git+https://github.com/openai/whisper.git
また、マイクの扱いに慣れているSpeechRecognitionをインストールしました。
$ pip install SpeechRecognition
動作環境
斜体が前回との差分です
- macOS
- CPU環境で動かしています
- Python 3.9.4
- ffmpeg 5.1.2
- https://github.com/openai/whisper/tree/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d をインストール
- torch 1.10.2
- transformers 4.22.2
- speech-recognition 3.8.1
結論:Whisperはマイクからも音声認識できます!
インメモリで扱うサンプルスクリプトです
from io import BytesIO import numpy as np import soundfile as sf import speech_recognition as sr import whisper if __name__ == "__main__": model = whisper.load_model("base") recognizer = sr.Recognizer() while True: # 「マイクから音声を取得」参照 with sr.Microphone(sample_rate=16_000) as source: print("なにか話してください") audio = recognizer.listen(source) print("音声処理中 ...") # 「音声データをWhisperの入力形式に変換」参照 wav_bytes = audio.get_wav_data() wav_stream = BytesIO(wav_bytes) audio_array, sampling_rate = sf.read(wav_stream) audio_fp32 = audio_array.astype(np.float32) result = model.transcribe(audio_fp32, fp16=False) print(result["text"])
「何度でも認識させよう」のモチベーション
「Whisperは、音声ファイルだけではなく、マイクから入力した音声も書き起こせるの?」というのは、多くの方が疑問に思うところではないかと思います。
実際discussionsには、マイクを通して使う項目が挙がっています。
マイクから入力した音声の認識、既存のアプローチ
上記のdiscussionで共有されるアプローチは、マイクから入力した音声を一度ファイルに保存してWhisperに渡す1と理解しています。
Whisperは音声ファイルを書き起こせるので、マイクからの音声も音声ファイルにすれば書き起こせるわけですね!
目的は達成できているので、これは素晴らしい実装だと思います👍
一方で、シオン・プロジェクトの経験から「一度音声ファイルに保存する」という点はブラッシュアップできると気付きました。
マイクから入力した音声の認識、提案したいアプローチ
これまでの経験から、「一度ファイルに出力するのは必須ではない」と考えています。
過去に、マイクから入力した音声を一時ファイルに保存 -> それを読み込んで音声認識という実装から、一時ファイルを除く修正をしました。
インメモリストリームで一時ファイルが不要と気づく
メモリと比べると、ファイルの読み書きは遅いと認識しています(ディスクアクセスのため)。
音声を何度でも認識させる際には、ファイルの読み書きをなくすことで処理は速くなり、よりよいユーザ体験となると考えます。
以上の理由で、私は音声をファイルに出力しない(インメモリで扱う)アプローチを推します。
音声データの渡し方を調査:transcribeメソッドは何をしているのか
ファイル出力を挟まず、データをメモリ上で変換してWhisperに文字起こしさせられるかを知るため、ソースコードを見ていきます。
出発点は環境構築の記事で動かした以下のコードです。
>>> import whisper >>> model = whisper.load_model("base") >>> result = model.transcribe("sample.wav", fp16=False)
load_model
関数でWhisperのモデルを読み込み、モデルのtranscribe
メソッドを呼んでいます。
whisper.load_model
load_model
関数は__init__.py
に定義されています。
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/__init__.py#L68
def load_model(name: str, device: Optional[Union[str, torch.device]] = None, download_root: str = None, in_memory: bool = False) -> Whisper:
型ヒントによると、返り値はWhisper
クラスのインスタンスです。
importを見ると、このクラスはmodel.py
に定義されている模様。
whisper.model.Whisper.transcribe
WhisperクラスはPyTorchのnn.Module
を継承したクラスとなっています2。
class Whisper(nn.Module):
transcribeメソッドの定義は以下
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/model.py#L266
transcribe = transcribe_function
importを見ると、transcribe_function
はtranscribe.py
のtranscribe
関数のようです。
whisper.transcribe.transcribe
transcribe
関数の定義を見ていきます。
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/transcribe.py#L19-L22
def transcribe( model: "Whisper", audio: Union[str, np.ndarray, torch.Tensor], *, # 以下略
私たちはmodel.transcribe("sample.wav")
と、音声ファイルのパスを表す文字列を実引数として渡す使い方を知っているのでした。
対応する仮引数は、上のコードのaudio
です。
型ヒントを見ると、文字列のほか、NumPy arrayやPyTorchのTensorも渡せることが分かります!3
ここで、音声ファイルのパスだけでなく、音声データ自体も渡せそうと見通しが立ちました。
どのように変換すればいいかが気になってきますが、もう少し仮引数audio
の扱いを見ていきましょう。
transcribe関数の実装の中でaudio
が登場するのは以下の箇所のみでした。
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/transcribe.py#L84
mel = log_mel_spectrogram(audio)
次に見るのはlog_mel_spectrogram
関数です。
importを見ると、audio.py
にあることが分かります。
whisper.audio.log_mel_spectrogram
log_mel_spectrogram
関数の定義はどうなっているのでしょうか。
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/audio.py#L92
def log_mel_spectrogram(audio: Union[str, np.ndarray, torch.Tensor], n_mels: int = N_MELS):
最初の仮引数audio
は文字列、NumPy array、PyTorchのTensorのいずれかを受け取ります4。
audio
の処理は以下のように行われます(audio
への再代入ですね):
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/audio.py#L109-L112
if not torch.is_tensor(audio): if isinstance(audio, str): audio = load_audio(audio) audio = torch.from_numpy(audio)
audio
が指す値が
- PyTorchのTensorの場合
- 上記の箇所は何もしません。Tensorのままです
- NumPyのarrayの場合
torch.from_numpy
でTensorに変わります
- strの場合
load_audio
関数(後述)によって読み込まれますload_audio
関数はNumPyのarrayを返します- 続く処理によってTensorに変わります
まとめると、上記4行の処理によりaudio
はPyTorchのTensor
に揃います。
whisper.audio.load_audio
ffmpegを使って音声ファイルを読み込む関数です。
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/audio.py#L22-L49
この関数を使う代わりに、マイクから取得した音声データを扱う方法を考えます。
マイクから取得した音声データの扱い方
マイクから音声を取得
マイクから音声を取得するのに、これまでのシオン・プロジェクトで慣れているSpeechRecognitionを使います。
import speech_recognition as sr r = sr.Recognizer() with sr.Microphone(sample_rate=16_000) as source: print("なにか話してください") audio = r.listen(source) print("音声を取得しました")
音声データをWhisperの入力形式に変換
これまでのシオン・プロジェクトから、SpeechRecognitionで取得した音声データをインメモリストリームを使ってNumPyのarrayに変換する方法は分かっています5。
この方法ではarrayのdtype
がnumpy.float64
です。
これに対して、whisperのload_audio
関数の返り値のNumPy arrayは、dtype
がnumpy.float32
です。
load_audio
関数に音声ファイルを渡して返り値のdtype
を確認しています6。
dtype
を揃える必要がありそうですよね。
numpy.float64
からnumpy.float32
に変換したNumpy arrayをtranscribe
メソッドに渡してみましょう。
from io import BytesIO import numpy as np import soundfile as sf import speech_recognition as sr r = sr.Recognizer() with sr.Microphone(sample_rate=16_000) as source: print("なにか話してください") audio = r.listen(source) print("音声処理中 ...") wav_bytes = audio.get_wav_data() wav_stream = BytesIO(wav_bytes) audio_array, sampling_rate = sf.read(wav_stream) audio_fp32 = audio_array.astype(np.float32) result = model.transcribe(audio_fp32, fp16=False)
Whisperは動き、音声認識しました!!🙌
ファイル出力を使った場合と同じ入力になっているか検証
SpeechRecognitionのMicrophoneを使って取得した音声データをNumPy arrayとして加工し、Whisperに渡す方法が分かりました。
最後に、音声データをファイル出力してパスをWhisperに渡す既存のアプローチと同じNumPy arrayになっているか検証します。
検証スクリプト
2つのNumPy arrayが等しいか、assert_array_equal
で検証します7。
- マイクから入力した音声を保存して読み込んだNumPy array(=
load_audio
関数の返り値) - インメモリストリームを使ったNumPy arrayの変換
検証結果
$ python validation.py 検証します。話してください 検証開始 (36864,) (36864,)
数パターン話してみましたが、NumPy arrayのassertionで例外は送出されませんでした。
Whisperでの認識結果と、この検証スクリプトから、メモリ上での音声データの変換は実装できていると結論づけました。
ただ、私自身は音声の扱いは入門者です。
この記事を読んで気になる点がありましたら、@ftnextまでフィードバックいただけますと大変助かります。
終わりに
Whisperでの音声認識、ファイルを渡すのではなく、マイクから入力した声を認識させる方法を見てきました。
パッとやるならファイル出力をしてそのファイルパスを渡す方法ですが、私が推すのはメモリ上で音声データ(NumPy array)を変換する方法です!
Whisperの実装をたどるなかでメモリで処理するアイデアが実現できそうということが見えてきて、実際に動かせ、検証も通った瞬間は、大きくガッツポーズでした8。
波形データの扱いはブラックボックスすぎるのですが、この実装の確度を高めるためにも「どこかでキャッチアップしたいな〜」と思っています。
例えばlog_mel_spectrogram
の単語の意味を理解したいですね。メル?? スペクトログラム??😵
学習リソースのオススメが浮かんだ方は教えていただけるととても助かります。
ここまででWhisperの扱いは完全に理解しました(※理論は後回しにしています)。
新章はこの後、どこへ進むのか? 理論を追うのか、Whisperを組み込んだシオンv0.0.2が誕生するのか、お楽しみに!
P.S. もう一つのモチベーション
SpeechRecognitionのコラボレータとして活動を始めているのですが、WhisperをサポートするPull requestをいただきました👏
この実装のアプローチも「一度音声ファイルに保存する」アプローチです(tempfile
使用)。
このプルリクエスト自体はやりたいことが実現できているのでマージできる(マージしたい)👍と考えますが、一方でこの記事で説明した、私自身の経験からのブラッシュアップも浮かぶので、そこはパッチとして提案していきたいとも思っています。
というわけで、この記事で紹介した実装は近日中にSpeechRecognitionに入れる考えです。お楽しみに!
-
https://github.com/mallorbc/whisper_mic/blob/9a4826d525890e6e5468048b58847328be48652b/mic.py#L40↩
-
https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/model.py#L197↩
-
docstringには「The path to the audio file to open, or the audio waveform(開くべき音声ファイルのパス、または音声波形)」ともあります↩
-
docstringには「The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz(音声のパスまたは、16kHzの音声波形を含むNumpy arrayかTensor)」とあります↩
-
この記事にまとめる中で
load_audio
関数でnumpy.float32
に変換しているのに気付きました https://github.com/openai/whisper/blob/9f70a352f9f8630ab3aa0d06af5cb9532bd8c21d/whisper/audio.py#L49↩ -
このトーク https://pycon.jp/2020/timetable/?id=203957 がオススメです。(WEB+DB PRESS連載「現場のPython」でも取り上げられていたかも)↩
-
脳内に響き渡った「やったなニッキー!!」「わあああああ👏👏👏👏👏」↩