WindowsでWhisperとpysummarizationを使って音声データの文字起こしと要約させてみた

# 概要
タイトル通り、音声データ（今回はwavファイル）を日本語に文字起こしさせて、文字起こしした文章をpythonで要約してみます。

想定している使い方としては、オンライン会議での簡易的な議事録作成などに使えればと思って作成しています。「会議での音声を録音して、それを文字起こしさせたいなー。できればその文章を要約してくれると助かるなー」というなまけ心が発動しました。

要約の方法は『抽出型』と『抽象型』の2つがありますが今回は```pysummarization```という神ライブラリを使うので、後者の抽象型の要約の仕方で実装します。
（抽出型の代表的なライブラリとして```sumy```というものがありますが今回はサクッと要約させたかったので```pysummarization```を使っています）


## 利用するライブラリ
- 文字起こしに使うライブラリ：OpenAIが公開したオープンソースの音声認識ソフトウェアである『Whisper』を使ってみます。
- 要約に使うのは、『pysummarization』というライブラリを使います。

今回利用しているライブラリをすべて下に載せておきます。
``` python
os
pickle
pandas
datetime
whisper
pysummarization
```

### ライブラリの入れ方
※Windowsで実行することを前提にしています。特にWhisperの入れ方は結構癖があるので、python環境がおかしくなる可能性あるので注意してください。（ちなみに筆者はwhisperの入れ方に失敗してanacondaを再インストールする羽目になりました。）
- whisperの入れ方：https://tadaoyamaoka.hatenablog.com/entry/2022/09/23/140401
- pysummarizationの入れ方：``` pip install pysummarization ```で入れます。


## コード
作成したコードの全文を載せます。

``` python
import os
import pickle
import pandas as pd
from datetime import datetime

import whisper


# インプットファイルや出力ファイルを定義
WAV_FILE = "./output.wav" # 文字起こし&要約したい音声ファイル
PKL_FILE = "./textise.pkl"
OUTPUT_TXT_FILE          = "./" + datetime.now().strftime('%Y%m%d_%H_%M') +".txt" # テキストファイルのファイル名を日付のtxtファイルにする
OUTPUT_ONE_SENTENCE_FILE = OUTPUT_ONE_SENTENCE_FILE = "./one_sentence.txt"
OUTPUT_SUMMARISED_FILE   = "./summarised.txt"



def whisper_to_textise_audio():
    """
    whisper ライブラリを使って、音声データをテキストにする
    その結果をresult変数という形で圧縮保存しておく
    """
    if os.path.exists(PKL_FILE):
        pass
    else:
        # tiny < base < small < medium < largeの順に高精度になるが、重くなる。(メモリ不足になる可能性がある)
        model  = whisper.load_model("base")
        result = model.transcribe(WAV_FILE, fp16=False, language="ja", verbose=True)
        
        with open(PKL_FILE, mode='wb') as f:
            pickle.dump(result, f)


def text_and_save():
    """
    whisperの結果ファイルpklをテキストファイル化して保存する
    今回は、文字起こしした結果の生データとその生データを一つの文字列化したものの2つをtxtファイル形式で保存する

    """

    with open(PKL_FILE, mode='rb') as f:
        result = pickle.load(f)

    # whisperのモデルが区切り文字（"。"や"!"など）を作らない場合があるので
    # 区切り文字を強制的に作成する必要がある。
    # そのためにpandasのDataFrameにいったん格納する。
    df_result     = pd.DataFrame(result["segments"])[["text"]]
    series_result = df_result['text']

    # 重複した文章が大量生成されている場合があるので、重複する要素があったら自動で重複排除させる
    list_result = series_result.values.tolist()
    list_result = list(set(list_result))

    # 区切り文字を追加してリストを一つの文字列strに直す。ここでは"。"を区切り文字とする。
    one_sentence = '。'.join(list_result)
    
    # 文字起こしの生データをtxtファイルとして保存
    df_result.to_csv(OUTPUT_TXT_FILE, header=False, index=False)

    # 一つの文字列化したものをtxtファイルとして保存しておく
    with open(OUTPUT_ONE_SENTENCE_FILE, 'w', encoding='utf-8', errors='ignore') as f: #txtファイルの新規作成
        f.write(one_sentence)


def summarise(N_output = 3):
    """
    文字列化したものを読み込んでそれを要約する。
    """
    from pysummarization.nlpbase.auto_abstractor import AutoAbstractor
    from pysummarization.tokenizabledoc.mecab_tokenizer import MeCabTokenizer
    from pysummarization.abstractabledoc.top_n_rank_abstractor import TopNRankAbstractor

    # "。"を区切り文字として一つの文章にしたファイルを読み込む。
    with open(OUTPUT_ONE_SENTENCE_FILE, encoding='utf-8', errors='ignore') as f:
        document = f.read()

    # 自動要約のオブジェクトを生成
    auto_abstractor = AutoAbstractor()

    # トークナイザーにMeCabを指定
    auto_abstractor.tokenizable_doc = MeCabTokenizer()

    # 文書の区切り文字を指定 <--これを使うために"。"を付与する処理をしていた
    auto_abstractor.delimiter_list = ["。", "\n"]

    # ドキュメントの抽象化、フィルタリングを行うオブジェクトを生成
    abstractable_doc = TopNRankAbstractor()

    # 文書の要約を実行
    result_dict = auto_abstractor.summarize(document, abstractable_doc)

    # 要約した文章は複数行になっているので、アウトプットする行数だけを保存するように変更する
    list_temp = result_dict["summarize_result"]
    if len(list_temp) > N_output:
        list_temp = list_temp[:N_output]
    
    list_output = []
    for line in list_temp:
        #list_output.append(repr(line.rstrip("\n")))
        list_output.append(line)

    with open(OUTPUT_SUMMARISED_FILE, 'w') as f:
        f.writelines(list_output)


def main():
    whisper_to_textise_audio()
    text_and_save()
    summarise()


if __name__ == '__main__':
    main()


```

## コードの解説

まず、処理の大きな流れは下のようなステップになっています。
1. 音声ファイルを読み込み、whisperを使って文字起こしする
2. 文字起こししたらテキストファイルとして保存する。このとき文字起こししたテキストを一つの文字列化したものも作成してそれも保存する
3. 文字列化した方のテキストファイルをインプットとしてpysummarizationで要約する。要約したら```N_output```の行数だけtxtファイルで保存する


## 結果
とあるyoutube動画の音声データを要約にかけてみた結果が下です。文字お越しした文章をそのまま掲載するのはいろいろ問題がありそうなので、要約結果だけを掲載します。ご容赦を。

```
縫い目とこのガッツリ外に出てるなんか。
でもね、持ってないからさ。
そういうicaid須部でえ、あっ、一emeおんなせいん。
```

全く意味が分かりません。ちなみにコードをもう一回実行すると下のようになりました。

```
なんかそれと鳴りとダンジョーが多かったと思うんだけど。
全然女時代に立って戦ってたけど。
家まで送ってもらいたいのみたいな。
```

どうやら結果は確率的にランダムになる様子。乱数のシードの問題なのか計算の仕方そのものにランダム性が入り込む余地があるのか。細かいところはわかりませんでした。


## 結果の考察
要約の精度自体はあまり高くない結果でした。ただし、これは次の3つが原因かもしれません。

1. 文字起こし自体の精度の問題
2. 話者が2人以上いること
3. 抽象型要約の限界

### 1 文字起こし自体の精度の問題
要約そのものの問題ではなく、要約対象としている文章そのものの質がイマイチだった可能性です。今回はwhisperのモデルの中で"base"を使って文字起こししましたが、その文字起こしの精度がイマイチだったところが原因として考えられます。

コードの途中にも記載していますが、"base"は下から数えた方が早いようにあまり精度の高いモデルではありません。

実際、```OUTPUT_ONE_SENTENCE_FILE```の中身をみると今回の文字起こしはその精度があまり高くなかったです。

これについては、"medium"や"large"を使えば解決できると思うのですが、その分計算時間が長くなるため、GPUを積んでいないマシンで実装するのはあまりお勧めできないかなと思います。

### 2 話者が2人以上いること
今回の音声ファイルは、2人がラジオ的に会話をするものを利用していました。これは2つのやりづらさを生んだと思います。

1つ目は文字起こしのしづらさです。2人が会話しているので、会話がかぶってしまっているところも多く、機械的に文字起こししづらいデータだったと思います。

2つ目は、要約のしづらさです。特定のトピックに限定せずに自然な流れで会話が発散していっているため、テキストの要約がしづらいタイプのデータだったのではないかと考えています。

したがって、文章の文字起こしは話者が1人だけになるタイプの音声データ（例えばニュースの読み上げやプレゼンテーションなど）が適しており、要約についても特定の一つのテーマについて語っている方が要約しやすいのだと思います。

### 抽象型要約の限界
「要約」についての知識があまりないのですが、一般的に抽象型要約は原文にはない表現を生成するため、文章として破綻しやすいと言われています。今回もそのパターンだったのではないかと思っています。

これについては、前述したように抽出型要約ライブラリの```sumy```と比較してみて結果を確認する必要がありますが、機械的な要約はまだまだ抽出型の方が精度が高いかもしれません。


## まとめ

- 音声データからの文字起こしはwhisperを使うと簡単に実装できる。精度を上げたい場合はパソコンのパワーが必要だが、モデルを変えることで簡単に対応できる点も魅力的。
- 要約についてはpysummarizationを使って簡単に実装できる。ただし、抽象型要約となるため、文章として破綻する場合もあるので、それを嫌うのであれば抽出型要約のsumyを使うのも手。