ReazonSpeechで全文文字起こしを実装してみた あとWhisperとの比較

# はじめに
[前回記事](https://qiita.com/user-touma/items/d010be623034ed894cdc)はサンプリングレートの問題で正常に動作しなかったようです。
教えてくださった方、ありがとうございました。
そしてReazon社やその関係者の皆様、不確実な情報を広めてしまい申し訳ございませんでした。

https://qiita.com/user-touma/items/d010be623034ed894cdc

# 本題
先日Reazon社から公開されたReazonSpeech(https://research.reazon.jp/projects/ReazonSpeech/) を使って、
長時間(1時間以上)の音声データの全文文字起こしを行ったので、コードを公開します。

# 問題意識
私の環境では、およそ60秒以上の音声を推論させると、メモリ不足のエラーを起こします。

```python:error
OutOfMemoryError: CUDA out of memory. 
Tried to allocate 14.66 GiB (GPU 0; 14.76 GiB total capacity;
946.41 MiB already allocated; 12.08 GiB free;
1.69 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try 
setting max_split_size_mb to avoid fragmentation.  
See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF
```

そこで、音声を30秒ごとに区切って処理するという[Whisper](https://openai.com/blog/whisper/)と同じ手法を取り入れました。
ただし30秒で機械的に区切っており、無音処理等はありません。

# やったこと
1. サンプリングレートを16000に変更(前回の反省) 
2. 音声を30秒ごとにカットし、それぞれで音声認識


# コード

以下のサイトを参考にしました。

https://tech.retrieva.jp/entry/2020/12/23/170645


https://www.hamlet-engineer.com/posts/ESPnetASR_long.html



:::note warn
決して綺麗なコードでないので、あくまで実験用とお考え下さい
:::

環境
Amazon Sage Maker(Jupyter Notebook)

<details><summary>モジュール等の準備</summary>

```python:qiita.rb
%%capture
!pip install -q espnet==0.10.3
!pip install -q espnet_model_zoo
import warnings
warnings.filterwarnings('ignore')

from huggingface_hub import notebook_login
notebook_login()

conda install -c conda-forge libsndfile
```
```python:qiita.rb
import torch
import librosa
import librosa.display
from IPython.display import display, Audio
import matplotlib.pyplot as plt
from datetime import datetime
import wave
import struct
import math
import os
from scipy import fromstring, int16
import subprocess
import shutil
import glob
import soundfile
from espnet2.bin.asr_inference import Speech2Text
from pydub import AudioSegment

device = "cuda" if torch.cuda.is_available() else "cpu"
beam_size = 5 #@param {type:"integer"}
```
```python:qiita.rb
#元のコードの名残で、意味はありません…
def mp4_to_wav(mp4f):
    wavf = mp4f
    return wavf
```
</details>

<details><summary>各種関数の定義</summary>

```python:qiita.rb
# 音声ファイルの分割(デフォルト30秒)
def cut_wav_espnet(wavf,time=30):
    # timeの単位は[sec]
    # ファイルを読み出し
    wr = wave.open(wavf, 'r')

    # waveファイルが持つ性質を取得
    ch = wr.getnchannels()
    width = wr.getsampwidth()
    fr = wr.getframerate()
    fn = wr.getnframes()
    total_time = 1.0 * fn / fr
    integer = math.floor(total_time) # 小数点以下切り捨て
    t = int(time)  # 秒数[sec]
    frames = int(ch * fr * t)
    num_cut = int(integer//t)

    # waveの実データを取得し、数値化
    data = wr.readframes(wr.getnframes())
    wr.close()
    X = fromstring(data, dtype=int16)
    
    # wavファイルを削除
    #os.remove(wavf)
    
    wavf_list = []
    for i in range(num_cut):
        # 出力データを生成
        output_dir = os.path.dirname(wavf) + 'output/cut_wav/'
        os.makedirs(output_dir,exist_ok=True)
        outf = output_dir + str(i).zfill(3) + '.wav'
        start_cut = i*frames
        end_cut = i*frames + frames
        Y = X[start_cut:end_cut]
        outd = struct.pack("h" * len(Y), *Y)

        # 書き出し
        ww = wave.open(outf, 'w')
        ww.setnchannels(ch)
        ww.setsampwidth(width)
        ww.setframerate(fr)
        ww.writeframes(outd)
        ww.close()
        
        # リストに追加
        wavf_list.append(outf)
    
    return wavf_list
```
```python:qiita.rb
# 複数ファイルの音声のテキスト変換
def wavs_asr_espnet(wavf_list, reazonspeech):
    output_text = ''
    # 複数処理
    print('音声のテキスト変換')
    for wavf in wavf_list:
        # wavファイルの読み込み
        speech, _ = soundfile.read(wavf)
        # 音声のテキスト変換
        nbests = reazonspeech(speech)
        text, *_ = nbests[0]
        print(text)

        # 各ファイルの出力結果の結合
        output_text = output_text + text + '\n\n'
        # wavファイルを削除
        os.remove(wavf)
        print(wavf)
        
    return output_text
```
```python:qiita.rb
# mp4からwavへの変換から音声のテキスト変換まで
def mp4_asr_espnet(mp4f):
    # mp4のディレクトリ
    input_dir = os.path.dirname(mp4f)
    # 学習済みをダウンロードし、音声認識モデルを作成
    reazonspeech = Speech2Text.from_pretrained(
        "reazon-research/reazonspeech-espnet-v1",
        beam_size=beam_size,
        batch_size=0,
        device=device
    )


    # 出力ディレクトリ
    if os.path.exists(input_dir + 'output/cut_wav/'):
        shutil.rmtree(input_dir + 'output/cut_wav/')
        os.makedirs(input_dir + 'output/cut_wav/', exist_ok=True)
    else:
        os.makedirs(input_dir + 'output/cut_wav/', exist_ok=True)
        
    
    
    # 音声ファイルへの変換
    wav_file = mp4_to_wav(mp4f)
    # 音声ファイルの分割(デフォルト30秒)
    cut_wavs = cut_wav_espnet(wav_file)
    
    # 複数ファイルの音声のテキスト変換
    out_text = wavs_asr_espnet(cut_wavs, reazonspeech)
    
    # テキストファイルへの入力
    mp4f_name = os.path.basename(mp4f)
    txt_file = input_dir + 'output/' + mp4f_name.replace('.wav', '.txt')
    print('テキスト出力')
    print(txt_file)
    f = open(txt_file, 'w')
    f.write(out_text)
    f.close()
```
</details>

実行部分
```python:qiita.rb
#ファイルをm4aからwav形式に変更
m4a_file = 'sample.m4a'
wav_filename = "sample.wav"
track = AudioSegment.from_file(m4a_file,  format= 'm4a')
file_handle = track.export(wav_filename, format='wav')

#サンプリングレートを16000に変更
speech, rate = soundfile.read(wav_filename)
orig_sr = rate
target_sr = 16000
wav, _ = librosa.load(wav_filename, sr=orig_sr)
resampled_wav = librosa.resample(wav, orig_sr, target_sr)
soundfile.write(wav_filename, resampled_wav, target_sr, 'PCM_16')

mp4_files = glob.glob(wav_filename)
for mp4_file in mp4_files:
    mp4_asr_espnet(mp4_file)
```
# 音声データ
今回は実験として約60分のYouTube動画の音声を文字起こししました。
動画のチョイスに特に意味はありません。

https://www.youtube.com/watch?v=5I4lMME21H4

※pydubでmp3からwavへ変換する過程でなぜか動画の始めの25分程がカットされており。実際に処理したのは本動画の25:00ごろ~です。原因は不明ですが、特に支障はないのでこのまま進めます。

# 実験結果
人力の文字起こし結果
>何時間ぐらいのやつだと、大体再生数これぐらいっていうのをわかるようにしようっていうので、付け始めたんですけど、あんま見ないんすよね。何かこう比較をし、僕あの比較するの好きなんですよ。数字を確認するのが、暇なときにね。
で、そのこういう時に、その、何曜日にやったほうがいいんだろう、何時にやった方がいいんだろうみたいなのを、こうバーとみるときに、その、並べると、そのすぐわかるようにっていうのにしようと思ったんですけど、あんまり意味ないすね。（略）


ReazonSpeechによる文字起こし結果 (出力ログをあえて残しています)
>これは何時間ぐらいのやつだと大体これぐらいっていう分かるようにしようっていうのでつけ始めたんですけどあんまり見ないんですよねその何かこう自覚をしているのが好きなんですよその数字を確認するのが今の時にね
output/cut_wav/000.wav
でこういう時にその何ていう日にやった方がいいんだろう何時にやった方がいいんだろうみたいなのをこうバッと見る時にこう並べるとすぐ分かるようにっていうのにしようと思ったんですけどあんまりですね
output/cut_wav/001.wav



30秒ごとにぶつ切られているため、冒頭に「これは」など幻聴が頻出するのですが、
基本的には精度良く文字起こしされているように見えます。
また処理時間は __28分__ でした。

<details><summary>おまけ：特におかしな部分抜粋</summary>

>そうですねはいはいはいはいはいはいはいはいはいはいはいはいはいっていうことはありますけどもっていうことはありますけどもっていうことなのかなっていうふうに思っていますのでそういうのをやっていただきたいなというふうに思っていているのかもしくはないのかなというのはあるのかなと思っていると思うのでそういうことですね
output/cut_wav/027.wav
そうですねはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいはいっていうのはこういうことなんですかなかなかないですよねこれはいいんですけれどもこれはいいんですよということですよね
output/cut_wav/054.wav


</details>


# Whisperとの比較
同じ音声をOpenAIのWhisperを使って文字起こしします。

人力の文字起こし結果

>何時間ぐらいのやつだと、大体再生数これぐらいっていうのをわかるようにしようっていうので、付け始めたんですけど、あんま見ないんすよね。何かこう比較をし、僕あの比較するの好きなんですよ。数字を確認するのが、暇なときにね。
で、そのこういう時に、その、何曜日にやったほうがいいんだろう、何時にやった方がいいんだろうみたいなのを、こうバーとみるときに、その、並べると、そのすぐわかるようにっていうのにしようと思ったんですけど、あんまり意味ないすね。（略）

Whisper large-v2による文字起こし結果
>[00:00.000-->00:04.280] 何時間くらいのやつだと だいたい再生数 これぐらいっていう分かる
[00:04.280-->00:08.140] ようにしようっていうので 付け はじめたんですけど あんま見ないん
[00:08.140-->00:14.000] ですよね 何か比較を 僕 比較するの好き なんですよ 数字を確認するの
[00:14.000-->00:18.480] が 決まるときにね こういうときに 何曜日にやったほうがいいんだろう
[00:18.480-->00:21.520] 何時にやったほうがいいんだろう みたいなのを ばーっと見るときに
[00:21.520-->00:27.220] 並べると すぐわかるようにっていう のにしようと思ったんですけど
[00:27.220-->00:34.100] あんまりいいんですね(略)

ほとんど完璧ですね。Whisperの精度には脱帽です。無音部分で音声分割が行われているらしく [^1] 、不自然な分割によるおかしな結果もなさそうです。

[^1]: https://dev.classmethod.jp/articles/whisper-how-to/

また推論には __13__ 分掛かりました。あれ…。(ReasonSpeech:28分)

# まとめと課題点
1. プレスリリースのとおり高精度に文字起こしが行われる(が、Whisperの方が精度が高そう)
2. Whisper Large(-v2)の方が推論が速い
3. 30秒ごとのぶつ切りによって総合的な推論の精度を著しく下げている点は今後の課題。


一つ断っておきたいのは、決してReazonSpeechが悪いということを言いたいわけではありません。個人としては、これを端緒に国産AIが一層盛り上がることを祈っています。
初心者ながら、いろいろいじるのを楽しませていただきました。本モデルに関わるReazon社、また関係者のみなさま、本当にありがとうございました。微力ながら、こういったツール開発に協力させていただきたく思います。


あとは、個人的な反省として、Githubでコードの共有ができたらよかったのですが、使ったことがなく、調べるのが億劫でした。良い指南書があれば教えてください…。