nikkie-ftnextの日記

イベントレポートや読書メモを発信

声をPythonに聴かせて(前編:wavファイルだと書き起こせるのに、マイクの入力はいまいち!?)

はじめに

私の熱い『アイの歌声を聴かせて』ファン活動、アイカツ、はじまります!

この記事は、アイの歌声を聴かせてに関する一技術者なりのファン活動の一幕です。

実は私、大好きなアイうたに出てくる"ポンコツAI"シオンを実装しようとしています。
今回は音声を認識してテキストに変換する方法について、これまでに分かったことを書き留めます。
クラウドベンダーが提供するAPIを使っていましたが、音声合成と同様に、機械学習モデルを使った実装に切り替えました(入門レベルであり、「やってみた系」です)。
アイうた劇中と重なる構成でアウトプットできることに気付いたため、2回に分けてお送りします。

目次

これまでのシオン・プロジェクト

  • シオン v0.0.1を3月末までに作るのを目標に進めています
  • 現在のシオン v0.0.1の実装
    1. 人が音声で入力
    2. 音声を文字に起こす
    3. 文字列を処理(今はオウム返し)
    4. 処理された文字列を読み上げる
  • 4の部分について、macOSsay コマンドを、ttslearn機械学習モデルを使った音声合成に更新しました

今回は2の部分をアップデートします。

音声を文字に起こす

これまでの実装:Cloud Speech-To-Text API

GoogleのCloud Speech-To-Text APIを使っていました。
https://cloud.google.com/speech-to-text

ライブラリ SpeechRecognition を使い、「マイクで受け取った音声をCloud Speech-To-Text APIに送り、返り値を受け取る」という実装です。

import speech_recognition as sr

r = sr.Recognizer()
with sr.Microphone() as source:
    print("なにか話してください")
    audio = r.listen(source)

recognized_text = r.recognize_google_cloud(
    audio, credentials_json=credentials_json, language="ja-JP"
)
print(recognized_text.strip())

SpeechRecognitionMicrophone はよくできていて、これだけのコードで プログラムに音声で入力 できます。
音声データはCloud Speech-To-Text APIに送られ、書き起こしたテキストが手に入ります。
なお、Cloud Speech-To-Text APIを使うための初期設定やcredentials_jsonの扱いは、ここでは省略しています。

これで最初に作る分には大成功1なのですが、シオンはスタンドアローン(通信できない)なので、APIを使わない方法を探す必要がありました。
そこで今回、機械学習モデルを使う方法に切り替えたわけです。

機械学習モデルを使った現在の実装

以下の学習済みモデルを利用しています。
https://huggingface.co/espnet/kan-bayashi_csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave

これをロードし、マイクで受け取った音声データを渡して、書き起こしたテキストを得ます。

順を追って、構成要素を紹介します。

espnet_model_zoo

ESPnetについては、kan-bayashiさん(ESPnetの開発者であり、上記のモデルの作成者)のチュートリアルが分かりやすいです。

また、同様の学習済みモデルをロードして使う例は、レトリバさんのブログにまとまっていました。

from espnet2.bin.asr_inference import Speech2Text

speech2text = Speech2Text.from_pretrained(
    "kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"
)

動作確認:sayコマンドでwavファイルを作る

レトリバさんのブログにある「親譲りの無鉄砲で子供の時から損ばかりしている」(『坊っちゃん』ですね)の音声ファイルは、macOSのsayコマンドで作りました2

$ say 親譲りの無鉄砲で子供の時から損ばかりしている -o sample.wav --data-format=LEF32@16000

@以降はサンプリングレートです(man say参照)。
今回のモデルは16000のデータで訓練されています3

SoundFileというライブラリを使って、wavファイルをNumPy arrayとして読み込みます。
学習済みモデルに渡すと、テキストが書き起こされます。

import soundfile

speech_array, sampling_rate = soundfile.read("sample.wav")

nbests = speech2text(speech_array)
text, tokens, *_ = nbests[0]
print(text)

Microphoneから取得した音声データを書き起こす

wavファイルの音声データを書き起こす方法は分かりました。
speech_recognition.Microphoneから取得した音声データもNumPy arrayにして学習済みモデルに渡せばよさそうですね。

Microphonelistenした返り値は、AudioData型です4
AudioDataインスタンスget_raw_dataメソッドで、音声データをbytes型で得られます5

これらをつなぎ合わせたのが以下のコード:

import numpy as np
import speech_recognition as sr
from espnet2.bin.asr_inference import Speech2Text

speech2text = Speech2Text.from_pretrained(
    "kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"
)

r = sr.Recognizer()
with sr.Microphone(sample_rate=16_000) as source:
    print("なにか話してください")
    audio_data = r.listen(source)

frame_bytes = audio_data.get_raw_data()
speech_array = np.frombuffer(frame_bytes, dtype=np.int16)

nbests = speech2text(speech_array)
text, tokens, *_ = nbests[0]
print(text)

np.frombufferはbytesをarrayに変えます。
https://numpy.org/doc/stable/reference/generated/numpy.frombuffer.html

ところが、「シオンが変だ!」

ワクワクしながらスクリプトを実行し、マイクから「こんにちは」と入力します。
坊っちゃん』の一節が書き起こせたのですから、「こんにちは」の書き起こしは余裕なはず!
ところが、現れたのは

'で'

🙄

「シオンが変だ!」

これは実装にバグがあったからなのですが、原因と対処は後編でお送りします(2022/02/19 後編へのリンクを追加しました)。

動作環境

  • macOS
  • CPU環境で動かしています
  • Python 3.9.4
  • SpeechRecognition 3.8.1
  • espnet-model-zoo 0.1.7
    • espnet 0.10.6
    • torch 1.10.2
  • SoundFile 0.10.3.post1